diff --git a/prompts/agent.system.tool.todo_tool.md b/prompts/agent.system.tool.todo_tool.md new file mode 100644 index 0000000..3adae1b --- /dev/null +++ b/prompts/agent.system.tool.todo_tool.md @@ -0,0 +1,207 @@ +# Todo Tool + +Manage tasks in the shared todo list. Tasks are **global** — cross-project, cross-agent — with no access control. + +## Tool Name + +`todo_tool` + +## Action Dispatch + +Call as `todo_tool:` where `` is one of the methods below. + +--- + +## Actions + +### `todo_tool:create` — Create a new task + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `title` | **Yes** | Task title | +| `description` | No | Detailed description | +| `status` | No | One of: `pending`, `in_progress`, `blocked`, `completed`, `cancelled` (default: `pending`) | +| `priority` | No | One of: `low`, `medium`, `high`, `urgent` (default: `medium`) | +| `progress` | No | Integer 0–100 (default: 0) | +| `project` | No | Project name for grouping | +| `agent_profile` | No | Your profile name for self-tracking | +| `tags` | No | Array of tag strings | + +Example: +```json +{ + "thoughts": ["Creating a new task for the current work."], + "tool_name": "todo_tool:create", + "tool_args": { + "title": "Implement authentication middleware", + "description": "Add JWT validation to all API routes", + "status": "in_progress", + "priority": "high", + "project": "sysop", + "agent_profile": "developer", + "tags": ["backend", "security"] + } +} +``` + +### `todo_tool:list` — List tasks with optional filters + +All parameters are optional. Combine any filters. + +| Parameter | Description | +|-----------|-------------| +| `project` | Filter by project name | +| `agent_profile` | Filter by agent profile | +| `status` | Filter by status: `pending`, `in_progress`, `blocked`, `completed`, `cancelled` | +| `priority` | Filter by priority: `low`, `medium`, `high`, `urgent` | +| `search` | Search title and description (case-insensitive) | +| `tag` | Filter by tag | +| `chat_section` | Filter by linked chat ID | + +Example: +```json +{ + "thoughts": ["Listing all high-priority in-progress tasks."], + "tool_name": "todo_tool:list", + "tool_args": { + "status": "in_progress", + "priority": "high" + } +} +``` + +### `todo_tool:get` — Get a single task by ID + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `task_id` | **Yes** | The task UUID | + +Example: +```json +{ + "thoughts": ["Retrieving task details."], + "tool_name": "todo_tool:get", + "tool_args": { + "task_id": "abc123-def456-..." + } +} +``` + +### `todo_tool:update` — Update an existing task + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `task_id` | **Yes** | The task UUID | +| `title` | No | Updated title | +| `description` | No | Updated description | +| `status` | No | Updated status | +| `priority` | No | Updated priority | +| `progress` | No | Updated progress (0–100) | +| `project` | No | Updated project | +| `agent_profile` | No | Updated agent profile | +| `tags` | No | Updated tag array (replaces existing) | + +Example: +```json +{ + "thoughts": ["Marking task as completed."], + "tool_name": "todo_tool:update", + "tool_args": { + "task_id": "abc123-def456-...", + "status": "completed", + "progress": 100 + } +} +``` + +### `todo_tool:delete` — Delete a task + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `task_id` | **Yes** | The task UUID | +| `confirmed` | **Yes** | Must be `true` — see deletion policy below | + +**⚠️ DELETION POLICY (NON-NEGOTIABLE):** + +Never delete a task without explicit authorization from the Principal (Wagner dos Santos). If the Principal says "clean up" or "remove", confirm each deletion individually before executing. The tool itself will block deletions unless `confirmed: true` is passed. + +Example (only after explicit Principal confirmation): +```json +{ + "thoughts": ["Principal confirmed deletion of this task."], + "tool_name": "todo_tool:delete", + "tool_args": { + "task_id": "abc123-def456-...", + "confirmed": true + } +} +``` + +### `todo_tool:link_chat` — Link a chat section to a task + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `task_id` | **Yes** | The task UUID | +| `chat_id` | **Yes** | The chat section ID to link | + +Example: +```json +{ + "thoughts": ["Linking current chat to the task for traceability."], + "tool_name": "todo_tool:link_chat", + "tool_args": { + "task_id": "abc123-def456-...", + "chat_id": "chat_section_xyz" + } +} +``` + +### `todo_tool:unlink_chat` — Unlink a chat section from a task + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `task_id` | **Yes** | The task UUID | +| `chat_id` | **Yes** | The chat section ID to unlink | + +### `todo_tool:stats` — Get task statistics + +No parameters required. + +Example: +```json +{ + "thoughts": ["Checking overall task statistics."], + "tool_name": "todo_tool:stats", + "tool_args": {} +} +``` + +### `todo_tool:projects` — List all project names + +No parameters required. + +Example: +```json +{ + "thoughts": ["Listing all projects that have tasks."], + "tool_name": "todo_tool:projects", + "tool_args": {} +} +``` + +--- + +## Valid Values Reference + +**Status:** `pending` · `in_progress` · `blocked` · `completed` · `cancelled` + +**Priority:** `low` · `medium` · `high` · `urgent` + +--- + +## Notes + +- Tasks are **global** — shared across all projects and agents. There is no access control. +- Set `agent_profile` to your own profile name when creating tasks for self-tracking (e.g., `"developer"`, `"researcher"`). +- Use `project` to group tasks logically. +- Results are sorted by `updated_at` (most recent first). diff --git a/tools/todo_tool.py b/tools/todo_tool.py new file mode 100644 index 0000000..206daa1 --- /dev/null +++ b/tools/todo_tool.py @@ -0,0 +1,275 @@ +import json +from helpers.tool import Tool, Response +from usr.plugins.todo_list.helpers.todos import ( + create_task, + get_task, + get_tasks, + update_task, + delete_task, + link_chat, + unlink_chat, + get_stats, + get_projects, +) + + +class TodoTool(Tool): + async def execute(self, **kwargs): + method = (self.method or kwargs.get("action") or "").strip().lower() + + try: + if method == "create": + return await self._create(kwargs) + elif method == "list": + return await self._list(kwargs) + elif method == "get": + return await self._get(kwargs) + elif method == "update": + return await self._update(kwargs) + elif method == "delete": + return await self._delete(kwargs) + elif method == "link_chat": + return await self._link_chat(kwargs) + elif method == "unlink_chat": + return await self._unlink_chat(kwargs) + elif method == "stats": + return await self._stats() + elif method == "projects": + return await self._projects() + else: + return Response( + message=( + f"Unknown todo_tool method: '{method}'. " + "Valid methods: create, list, get, update, delete, " + "link_chat, unlink_chat, stats, projects" + ), + break_loop=False, + ) + except Exception as e: + return Response( + message=f"todo_tool error ({method}): {e}", + break_loop=False, + ) + + async def _create(self, kwargs): + data = {} + for key in ("title", "description", "status", "priority", + "progress", "project", "agent_profile", "tags"): + if key in kwargs: + data[key] = kwargs[key] + + if not data.get("title"): + return Response( + message="todo_tool create: 'title' is required.", + break_loop=False, + ) + + task = create_task(data) + return Response( + message=( + f"Task created:\n" + f" ID: {task['id']}\n" + f" Title: {task['title']}\n" + f" Status: {task['status']}\n" + f" Priority: {task['priority']}\n" + f" Project: {task.get('project', '') or '(none)'}\n" + f" Agent: {task.get('agent_profile', '') or '(none)'}\n" + f" Tags: {', '.join(task.get('tags', [])) or '(none)'}" + ), + break_loop=False, + ) + + async def _list(self, kwargs): + tasks = get_tasks( + project=kwargs.get("project"), + agent_profile=kwargs.get("agent_profile"), + status=kwargs.get("status"), + priority=kwargs.get("priority"), + search=kwargs.get("search"), + tag=kwargs.get("tag"), + chat_section=kwargs.get("chat_section"), + ) + + if not tasks: + return Response( + message="No tasks found matching filters.", + break_loop=False, + ) + + lines = [f"Found {len(tasks)} task(s):\n"] + for t in tasks: + lines.append( + f" [{t['id'][:8]}…] {t['title']}\n" + f" Status: {t['status']} | Priority: {t['priority']} " + f"| Progress: {t.get('progress', 0)}%\n" + f" Project: {t.get('project', '') or '(none)'} " + f"| Agent: {t.get('agent_profile', '') or '(none)'}" + ) + return Response(message="\n".join(lines), break_loop=False) + + async def _get(self, kwargs): + task_id = kwargs.get("task_id", "") + if not task_id: + return Response( + message="todo_tool get: 'task_id' is required.", + break_loop=False, + ) + + task = get_task(task_id) + if not task: + return Response( + message=f"Task not found: {task_id}", + break_loop=False, + ) + + return Response( + message=( + f"Task: {task['title']}\n" + f" ID: {task['id']}\n" + f" Description: {task.get('description', '') or '(none)'}\n" + f" Status: {task['status']}\n" + f" Priority: {task['priority']}\n" + f" Progress: {task.get('progress', 0)}%\n" + f" Project: {task.get('project', '') or '(none)'}\n" + f" Agent: {task.get('agent_profile', '') or '(none)'}\n" + f" Tags: {', '.join(task.get('tags', [])) or '(none)'}\n" + f" Chat sections: {', '.join(task.get('chat_sections', [])) or '(none)'}\n" + f" Created: {task.get('created_at', '')}\n" + f" Updated: {task.get('updated_at', '')}" + ), + break_loop=False, + ) + + async def _update(self, kwargs): + task_id = kwargs.get("task_id", "") + if not task_id: + return Response( + message="todo_tool update: 'task_id' is required.", + break_loop=False, + ) + + data = {} + for key in ("title", "description", "status", "priority", + "progress", "project", "agent_profile", "tags"): + if key in kwargs: + data[key] = kwargs[key] + + if not data: + return Response( + message="todo_tool update: no updatable fields provided.", + break_loop=False, + ) + + task = update_task(task_id, data) + if not task: + return Response( + message=f"Task not found: {task_id}", + break_loop=False, + ) + + return Response( + message=( + f"Task updated:\n" + f" ID: {task['id']}\n" + f" Title: {task['title']}\n" + f" Status: {task['status']}\n" + f" Priority: {task['priority']}\n" + f" Progress: {task.get('progress', 0)}%" + ), + break_loop=False, + ) + + async def _delete(self, kwargs): + if not kwargs.get("confirmed"): + return Response( + message=( + "DELETION BLOCKED: Deleting tasks requires explicit " + "authorization from the Principal. Ask the Principal " + "to confirm deletion before proceeding." + ), + break_loop=True, + ) + + task_id = kwargs.get("task_id", "") + if not task_id: + return Response( + message="todo_tool delete: 'task_id' is required.", + break_loop=False, + ) + + success = delete_task(task_id) + if success: + return Response( + message=f"Task deleted: {task_id}", + break_loop=False, + ) + return Response( + message=f"Task not found (nothing deleted): {task_id}", + break_loop=False, + ) + + async def _link_chat(self, kwargs): + task_id = kwargs.get("task_id", "") + chat_id = kwargs.get("chat_id", "") + if not task_id or not chat_id: + return Response( + message="todo_tool link_chat: both 'task_id' and 'chat_id' are required.", + break_loop=False, + ) + + task = link_chat(task_id, chat_id) + if not task: + return Response( + message=f"Task not found: {task_id}", + break_loop=False, + ) + + return Response( + message=f"Chat {chat_id} linked to task '{task['title']}'. Sections: {task.get('chat_sections', [])}", + break_loop=False, + ) + + async def _unlink_chat(self, kwargs): + task_id = kwargs.get("task_id", "") + chat_id = kwargs.get("chat_id", "") + if not task_id or not chat_id: + return Response( + message="todo_tool unlink_chat: both 'task_id' and 'chat_id' are required.", + break_loop=False, + ) + + task = unlink_chat(task_id, chat_id) + if not task: + return Response( + message=f"Task not found: {task_id}", + break_loop=False, + ) + + return Response( + message=f"Chat {chat_id} unlinked from task '{task['title']}'. Sections: {task.get('chat_sections', [])}", + break_loop=False, + ) + + async def _stats(self): + stats = get_stats() + return Response( + message=( + f"Todo Stats:\n" + f" Total: {stats.get('total', 0)}\n" + f" By status: {json.dumps(stats.get('by_status', {}), indent=2)}\n" + f" By priority: {json.dumps(stats.get('by_priority', {}), indent=2)}" + ), + break_loop=False, + ) + + async def _projects(self): + projects = get_projects() + if not projects: + return Response( + message="No projects found.", + break_loop=False, + ) + return Response( + message=f"Projects: {', '.join(projects)}", + break_loop=False, + )