{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAsBhD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CA2K9E","sourcesContent":["import type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\nimport type { TodoRuntime } from \"./runtime.ts\";\nimport type { TodoDefinition, TodoListDocument, TodoListView, TodoTask } from \"./types.ts\";\n\nconst TodoId = Type.String({\n\tdescription: \"Stable task id, such as T1\",\n\tpattern: \"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$\",\n});\nconst Subject = Type.String({ minLength: 1, maxLength: 500 });\nconst Description = Type.String({ maxLength: 10_000 });\nconst ActiveForm = Type.String({ maxLength: 500 });\nconst AcceptanceCriterion = Type.String({ minLength: 1, maxLength: 2000 });\nconst MAX_LIST_TASKS = 50;\nconst MAX_LIST_TEXT = 8000;\nconst TodoDefinitionSchema = Type.Object({\n\tid: TodoId,\n\tsubject: Subject,\n\tdescription: Type.Optional(Description),\n\tactive_form: Type.Optional(ActiveForm),\n\tdepends_on: Type.Array(TodoId),\n\tacceptance_criteria: Type.Optional(Type.Array(AcceptanceCriterion)),\n});\n\nexport function registerTodoTools(pi: ExtensionAPI, runtime: TodoRuntime): void {\n\tpi.registerTool({\n\t\tname: \"write_todo\",\n\t\tlabel: \"Import Todo Plan\",\n\t\tdescription: \"Replace the current todo list with a validated dependency graph\",\n\t\tpromptSnippet: \"Create a persistent dependency-aware todo list from an execution plan\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use write_todo when a [PI TODO PLAN] message asks you to structure confirmed multi-step work.\",\n\t\t\t\"Do not add mandatory review tasks. Add an ordinary review task only when review is useful for this work.\",\n\t\t],\n\t\tparameters: Type.Object({\n\t\t\titems: Type.Array(TodoDefinitionSchema, { minItems: 1 }),\n\t\t\tglobal_direction: Type.String({ minLength: 1, maxLength: 20_000 }),\n\t\t}),\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst list = await runtime.replace(params.global_direction, params.items as TodoDefinition[]);\n\t\t\treturn result(\n\t\t\t\t`Created ${list.tasks.length} tasks. Call todo_list to inspect ready work, then claim tasks before execution.`,\n\t\t\t\tdocumentDetails(list),\n\t\t\t);\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_create\",\n\t\tlabel: \"Create Todo\",\n\t\tdescription: \"Add one or more tasks to the active list, or create a list when global_direction is supplied\",\n\t\tpromptSnippet: \"Dynamically add tasks to the todo list\",\n\t\tpromptGuidelines: [\"Add newly discovered work as normal tasks rather than replacing the active list.\"],\n\t\tparameters: Type.Object({\n\t\t\titems: Type.Array(TodoDefinitionSchema, { minItems: 1 }),\n\t\t\tglobal_direction: Type.Optional(Type.String({ minLength: 1, maxLength: 20_000 })),\n\t\t}),\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst list = await runtime.add(params.items as TodoDefinition[], params.global_direction);\n\t\t\treturn result(`Added ${params.items.length} task(s).`, documentDetails(list));\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_list\",\n\t\tlabel: \"List Todos\",\n\t\tdescription: \"List active, ready, blocked, and completed tasks\",\n\t\tpromptSnippet: \"Inspect todo state and ready work\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use todo_list after interruption or when selecting the next task.\",\n\t\t\t\"Several ready tasks may be claimed independently; there is no wave barrier.\",\n\t\t\t\"When at least two ready tasks are independent and have separate file ownership, claim them and delegate them in one background agent_start batch using worker.\",\n\t\t],\n\t\tparameters: Type.Object({ ready_only: Type.Optional(Type.Boolean()) }),\n\t\texecutionMode: \"parallel\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst view = await runtime.view();\n\t\t\tif (!view) return result(\"No todo list is active.\", undefined);\n\t\t\tconst tasks = params.ready_only ? view.ready : view.list.tasks;\n\t\t\treturn result(formatList(view, tasks), listDetails(view, tasks));\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_get\",\n\t\tlabel: \"Get Todo\",\n\t\tdescription: \"Get complete details for one task\",\n\t\tpromptSnippet: \"Inspect one todo task\",\n\t\tparameters: Type.Object({ id: TodoId }),\n\t\texecutionMode: \"parallel\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst task = await runtime.getTask(params.id);\n\t\t\tif (!task) throw new Error(`Todo \"${params.id}\" does not exist`);\n\t\t\treturn result(formatTask(task), task);\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_update\",\n\t\tlabel: \"Update Todo\",\n\t\tdescription:\n\t\t\t\"Update task content, dependencies, or public status. A return starting with REJECTED means the operation was rejected, not successful.\",\n\t\tpromptSnippet: \"Update or complete a todo task\",\n\t\tpromptGuidelines: [\n\t\t\t\"Public task states are pending, in_progress, and completed.\",\n\t\t\t\"A worker completes its task explicitly after satisfying acceptance criteria; agent success alone is not completion.\",\n\t\t],\n\t\tparameters: Type.Object({\n\t\t\tid: TodoId,\n\t\t\tsubject: Type.Optional(Subject),\n\t\t\tdescription: Type.Optional(Type.Union([Description, Type.Null()])),\n\t\t\tactive_form: Type.Optional(Type.Union([ActiveForm, Type.Null()])),\n\t\t\tdepends_on: Type.Optional(Type.Array(TodoId)),\n\t\t\tacceptance_criteria: Type.Optional(Type.Union([Type.Array(AcceptanceCriterion), Type.Null()])),\n\t\t\tstatus: Type.Optional(\n\t\t\t\tType.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")]),\n\t\t\t),\n\t\t\texpected_revision: Type.Optional(Type.Integer({ minimum: 1 })),\n\t\t}),\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst list = await runtime.update(params.id, { ...params });\n\t\t\tconst task = requireTask(list, params.id);\n\t\t\tconst cleared = list.tasks.length > 0 && list.tasks.every((candidate) => candidate.status === \"completed\");\n\t\t\tconst note = cleared ? \" All tasks completed; list cleared.\" : \"\";\n\t\t\treturn result(`${params.id} is ${task.status} at revision ${task.revision}.${note}`, task);\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_claim\",\n\t\tlabel: \"Claim Todo\",\n\t\tdescription:\n\t\t\t\"Atomically claim one dependency-ready task. A return starting with REJECTED means the operation was rejected, not successful.\",\n\t\tpromptSnippet: \"Claim a ready todo task before starting work\",\n\t\tpromptGuidelines: [\n\t\t\t\"Pass expected_revision when coordinating concurrent workers to reject stale claims.\",\n\t\t\t\"Copy the returned metadata unchanged into the matching agent_start item.\",\n\t\t],\n\t\tparameters: Type.Object({\n\t\t\tid: TodoId,\n\t\t\towner: Type.Optional(Type.String({ minLength: 1 })),\n\t\t\texpected_revision: Type.Optional(Type.Integer({ minimum: 1 })),\n\t\t}),\n\t\texecutionMode: \"parallel\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst claim = await runtime.claim(params.id, params.owner, params.expected_revision);\n\t\t\tconst listId = runtime.getListId();\n\t\t\tif (!listId) throw new Error(\"No todo list is active\");\n\t\t\tconst metadata = {\n\t\t\t\t\"pi.todo/list-id\": listId,\n\t\t\t\t\"pi.todo/task-id\": params.id,\n\t\t\t};\n\t\t\tconst batchItem = { agent: \"worker\", task: claim.task.subject, metadata };\n\t\t\treturn result(\n\t\t\t\t`Claimed ${params.id} for ${claim.task.owner}. Agent batch item:\\n${JSON.stringify(batchItem)}`,\n\t\t\t\t{\n\t\t\t\t\t...claim,\n\t\t\t\t\tmetadata,\n\t\t\t\t\tagent_start_item: batchItem,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_release\",\n\t\tlabel: \"Release Todo\",\n\t\tdescription: \"Return an in-progress task to pending\",\n\t\tpromptSnippet: \"Return unfinished claimed work to pending\",\n\t\tparameters: Type.Object({ id: TodoId }),\n\t\texecutionMode: \"parallel\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst list = await runtime.release(params.id);\n\t\t\treturn result(\n\t\t\t\t`Released ${params.id}; it is ready when its dependencies are complete.`,\n\t\t\t\trequireTask(list, params.id),\n\t\t\t);\n\t\t},\n\t});\n\n\tpi.registerTool({\n\t\tname: \"todo_delete\",\n\t\tlabel: \"Delete Todo\",\n\t\tdescription: \"Delete an unneeded task and remove it from dependent task edges\",\n\t\tpromptSnippet: \"Remove an unneeded todo task\",\n\t\tparameters: Type.Object({ id: TodoId }),\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params) {\n\t\t\tconst list = await runtime.delete(params.id);\n\t\t\treturn result(`Deleted ${params.id} and cleaned dependent edges.`, documentDetails(list));\n\t\t},\n\t});\n}\n\nfunction result<T>(text: string, details: T) {\n\treturn { content: [{ type: \"text\" as const, text }], details };\n}\n\nfunction formatList(view: TodoListView, tasks: readonly TodoTask[]): string {\n\tconst summary = view.summary;\n\tconst heading = `${summary.completed}/${summary.total} completed; ${summary.in_progress} active; ${summary.ready} ready; ${summary.blocked} blocked`;\n\tif (!tasks.length) return `${heading}\\nNo matching tasks.`;\n\tconst visible = tasks.slice(0, MAX_LIST_TASKS).map(formatTask);\n\tif (tasks.length > visible.length) visible.push(`... ${tasks.length - visible.length} more tasks`);\n\treturn truncate(`${heading}\\n${visible.join(\"\\n\")}`, MAX_LIST_TEXT);\n}\n\nfunction formatTask(task: TodoTask): string {\n\tconst owner = task.owner ? ` owner=${task.owner}` : \"\";\n\tconst dependencies = task.depends_on.length ? ` depends_on=${task.depends_on.join(\",\")}` : \"\";\n\treturn `[${task.status}] ${task.id}: ${task.subject}${owner}${dependencies} revision=${task.revision}`;\n}\n\nfunction requireTask(list: TodoListDocument, id: string): TodoTask {\n\tconst task = list.tasks.find((candidate) => candidate.id === id);\n\tif (!task) throw new Error(`Todo \"${id}\" does not exist`);\n\treturn task;\n}\n\nfunction documentDetails(list: TodoListDocument) {\n\treturn { list_id: list.id, revision: list.revision, task_count: list.tasks.length };\n}\n\nfunction listDetails(view: TodoListView, tasks: readonly TodoTask[]) {\n\tconst visible = tasks.slice(0, MAX_LIST_TASKS).map((task) => ({\n\t\tid: task.id,\n\t\tsubject: truncate(task.subject, 500),\n\t\tstatus: task.status,\n\t\towner: task.owner,\n\t\tdepends_on: task.depends_on,\n\t\trevision: task.revision,\n\t}));\n\treturn {\n\t\tlist_id: view.list.id,\n\t\trevision: view.list.revision,\n\t\tglobal_direction: truncate(view.list.global_direction, 1000),\n\t\tsummary: view.summary,\n\t\ttasks: visible,\n\t\ttruncated_tasks: tasks.length - visible.length,\n\t};\n}\n\nfunction truncate(value: string, limit: number): string {\n\treturn value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;\n}\n"]}