@ai-agent-forge/plugin-todo-set 0.88.14 → 0.88.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,22 @@ Todos persist as append-only `todo.state` session entries and every change is an
8
8
  soft reference is advisory (#10): an unknown `planId` never fails the write — the tool response carries a `warning`
9
9
  field instead, read from the session branch's latest `plan.state` snapshot through the shared session entry protocol.
10
10
 
11
+ ## Status contract
12
+
13
+ A todo record carries one of four statuses:
14
+
15
+ - `pending` — open work.
16
+ - `blocked` — temporarily obstructed and recoverable (may return to `pending`).
17
+ - `done` — completed with evidence. Terminal: further `todo_update` calls on the id are rejected.
18
+ - `cancelled` — voided: judged no longer needed, permanently retired from execution. Terminal, with the same
19
+ no-further-update guard as `done`.
20
+
21
+ Both `done` and `cancelled` require at least one `evidence` reference (two independent gates, one error message each;
22
+ for `cancelled`, cite the user's words that retired the todo): done's evidence guards against fabricated completion,
23
+ cancelled's evidence guards against unmandated cancellation. The `/todo` count line lists its non-zero segments in
24
+ `done → blocked → cancelled` order (e.g. `todos: 6 (2 done, 1 cancelled)`) and each record line shows its status
25
+ marker, so a cancelled todo renders as `- <id> [cancelled → <planId>] <title>`.
26
+
11
27
  ## Install
12
28
 
13
29
  ```sh
package/agent-forge.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageFormatVersion": 1,
3
3
  "id": "agent-forge.plugin.todo-set",
4
- "pluginVersion": "0.88.14",
4
+ "pluginVersion": "0.88.16",
5
5
  "apiVersion": "1",
6
6
  "minHostVersion": "0.84.4",
7
7
  "platform": {
package/dist/index.d.ts CHANGED
@@ -18,7 +18,11 @@ export interface TodoRecord {
18
18
  /** Soft reference: a plain string; unknown ids yield a response warning, not a failure. */
19
19
  planId: string;
20
20
  title: string;
21
- status: "pending" | "done" | "blocked";
21
+ /**
22
+ * Lifecycle status. `cancelled` is terminal (voided: permanently retired,
23
+ * no longer part of execution) and shares done's no-further-update guard.
24
+ */
25
+ status: "pending" | "done" | "blocked" | "cancelled";
22
26
  revision: number;
23
27
  evidence: string[];
24
28
  }
@@ -27,7 +31,9 @@ export interface TodoState {
27
31
  }
28
32
  /**
29
33
  * `/todo` command text rendering: one line per todo (`id [status → planId]
30
- * title`) plus the evidence lines that justified completion. Plain text only —
34
+ * title`) plus the evidence lines that justified completion. The leading count
35
+ * line lists its non-zero segments in done → blocked → cancelled order. Plain
36
+ * text only —
31
37
  * every host shape can render it, no TUI required (the same L1 contract as
32
38
  * the workflow plugin's `/workflow`). Collapsed tool blocks pass
33
39
  * `{ evidence: false }` to omit the evidence lines; the default keeps the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGrF,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;CACpB;AA6DD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAe1F;AAyMD,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,aAAa,GAChB,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC,GAAG,SAAS,sBAAsB,EAAE,CAgEhF","sourcesContent":["/**\n * Todo-set capability plugin: a self-contained todo state machine, extracted\n * from the host's `src/capabilities/todo-set.ts` (D-075 S4, first B-class\n * batch).\n *\n * Todos persist as append-only `todo.state` session entries and every change\n * is announced as a `todo.changed` event. `planId` is a soft reference: a\n * plain string whose target is checked ADVISORILY against the plan plugin's\n * LATEST `plan.state` snapshot (the same latest-snapshot semantics as this\n * plugin's own {@link loadState}); an unknown id yields a response warning,\n * never a failure (the same #10 pattern as the plan plugin's goalId check).\n * The plugin only consumes the public `session`, `events`, and `tools`\n * capabilities and never writes state owned by another plugin.\n */\n\nimport type { CapabilityAPI, DisposableRegistration } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\n\nexport interface TodoRecord {\n\tid: string;\n\t/** Soft reference: a plain string; unknown ids yield a response warning, not a failure. */\n\tplanId: string;\n\ttitle: string;\n\tstatus: \"pending\" | \"done\" | \"blocked\";\n\trevision: number;\n\tevidence: string[];\n}\n\nexport interface TodoState {\n\ttodos: TodoRecord[];\n}\n\nconst stateEntry = \"todo.state\";\n/** Session protocol key of the plan plugin's snapshot entry (plugins/plan). */\nconst planStateEntry = \"plan.state\";\n\nconst todoSchema = Type.Object({\n\tid: Type.String({ description: \"Stable id of the todo to create or update\" }),\n\tplanId: Type.String({\n\t\tdescription:\n\t\t\t\"Soft reference to a plan id; an id unknown to this session's plan state yields a response warning, not a failure\",\n\t}),\n\ttitle: Type.String({ description: \"Human-readable todo title\" }),\n\trevision: Type.Integer({\n\t\tdescription: \"New revision counter; must be greater than the stored revision of an existing todo\",\n\t\tminimum: 0,\n\t}),\n\tstatus: Type.Optional(\n\t\tType.Union([Type.Literal(\"pending\"), Type.Literal(\"done\"), Type.Literal(\"blocked\")], {\n\t\t\tdescription: \"Next status; defaults to the stored status for existing todos, otherwise pending\",\n\t\t}),\n\t),\n\tevidence: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription: \"Evidence references; at least one is required when status is done\",\n\t\t}),\n\t),\n});\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Todo input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction text(value: unknown, name: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${name} must be a non-empty string`);\n\treturn value;\n}\n\nfunction revision(value: unknown): number {\n\tif (!Number.isInteger(value) || (value as number) < 0) throw new Error(\"revision must be a non-negative integer\");\n\treturn value as number;\n}\n\nfunction evidence(value: unknown): string[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value) || value.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new Error(\"evidence must be a string array\");\n\t}\n\treturn [...(value as string[])];\n}\n\nfunction loadState(api: CapabilityAPI): TodoState {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tconst entry = [...entries].reverse().find((item) => item.customType === stateEntry);\n\tif (!entry?.data || typeof entry.data !== \"object\") return { todos: [] };\n\tconst data = entry.data as Partial<TodoState>;\n\treturn { todos: Array.isArray(data.todos) ? [...data.todos] : [] };\n}\n\n/**\n * `/todo` command text rendering: one line per todo (`id [status → planId]\n * title`) plus the evidence lines that justified completion. Plain text only —\n * every host shape can render it, no TUI required (the same L1 contract as\n * the workflow plugin's `/workflow`). Collapsed tool blocks pass\n * `{ evidence: false }` to omit the evidence lines; the default keeps the\n * command output byte-for-byte stable.\n */\nexport function renderTodoState(state: TodoState, options?: { evidence?: boolean }): string {\n\tconst withEvidence = options?.evidence ?? true;\n\tif (state.todos.length === 0) return \"no todos in this session; create one with todo_update\";\n\tconst done = state.todos.filter((todo) => todo.status === \"done\").length;\n\tconst blocked = state.todos.filter((todo) => todo.status === \"blocked\").length;\n\tconst counts = blocked > 0 ? `${done} done, ${blocked} blocked` : `${done} done`;\n\tconst lines = [`todos: ${state.todos.length} (${counts})`];\n\t// Record fields only validate non-emptiness, so fold newlines to keep the\n\t// one-line-per-record shape whatever the model wrote.\n\tconst inline = (value: string): string => value.replace(/\\n/g, \" \");\n\tfor (const todo of state.todos) {\n\t\tlines.push(`- ${inline(todo.id)} [${todo.status} → ${inline(todo.planId)}] ${inline(todo.title)}`);\n\t\tif (withEvidence) for (const item of todo.evidence) lines.push(` evidence: ${inline(item)}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Structural slices of the host tool-render contracts the renderer needs.\n * Declared locally on purpose: this plugin must stay self-contained and must\n * not import `@agent-forge/tui` (the tool-definition.ts structural hosting\n * boundary), so renderResult talks to these shapes instead of the real\n * Theme/Component types.\n */\ninterface ToolResultTextPartLike {\n\treadonly type: string;\n\treadonly text?: string;\n}\n\ninterface ToolResultLike {\n\treadonly content: readonly ToolResultTextPartLike[];\n}\n\ninterface RenderResultOptionsLike {\n\treadonly expanded: boolean;\n\treadonly isPartial: boolean;\n}\n\ninterface ThemeLike {\n\tfg(color: string, text: string): string;\n}\n\ninterface RenderContextLike {\n\treadonly lastComponent?: unknown;\n}\n\n/**\n * Simplified East Asian width table: these ranges cover the CJK blocks state\n * titles realistically use and count as 2 columns each; every other code\n * point counts as 1. Not a full wcwidth — combining marks and ambiguous\n * widths fall back to 1, the safe (narrower) default for row folding.\n */\nfunction isWideCodePoint(code: number): boolean {\n\treturn (\n\t\t(code >= 0x1100 && code <= 0x115f) ||\n\t\t(code >= 0x2e80 && code <= 0xa4cf) ||\n\t\t(code >= 0xac00 && code <= 0xd7a3) ||\n\t\t(code >= 0xf900 && code <= 0xfaff) ||\n\t\t(code >= 0xfe30 && code <= 0xfe4f) ||\n\t\t(code >= 0xff00 && code <= 0xff60) ||\n\t\t(code >= 0xffe0 && code <= 0xffe6) ||\n\t\t(code >= 0x20000 && code <= 0x3fffd)\n\t);\n}\n\n/** Folds one logical line into rows of at most `width` display columns. */\nfunction wrapLine(line: string, width: number): string[] {\n\tif (width <= 0) return [line];\n\tconst rows: string[] = [];\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of line) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (row !== \"\" && columns + cost > width) {\n\t\t\trows.push(row);\n\t\t\trow = character;\n\t\t\tcolumns = cost;\n\t\t} else {\n\t\t\trow += character;\n\t\t\tcolumns += cost;\n\t\t}\n\t}\n\trows.push(row);\n\treturn rows;\n}\n\n/**\n * Structural TUI component rendering a todo_update result as the plain state\n * listing: `warning: ` lines take the host theme's warning color, every other\n * row toolOutput (color is decided per logical line, so a wrapped warning\n * stays warning-colored). Rows are cached per width — the host may render the\n * same result repeatedly at one width — and dropped on text updates or\n * invalidate.\n */\nclass TodoResultComponent {\n\tprivate text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly rowsByWidth = new Map<number, string[]>();\n\n\tconstructor(text: string, theme: ThemeLike) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t}\n\n\tsetText(text: string): void {\n\t\tif (text === this.text) return;\n\t\tthis.text = text;\n\t\tthis.rowsByWidth.clear();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst cached = this.rowsByWidth.get(width);\n\t\tif (cached) return cached;\n\t\tconst rows: string[] = [];\n\t\tfor (const line of this.text.split(\"\\n\")) {\n\t\t\tconst color = line.startsWith(\"warning: \") ? \"warning\" : \"toolOutput\";\n\t\t\tfor (const row of wrapLine(line, width)) rows.push(this.theme.fg(color, row));\n\t\t}\n\t\tthis.rowsByWidth.set(width, rows);\n\t\treturn rows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.rowsByWidth.clear();\n\t}\n}\n\n/**\n * Parses the text part the host serialized from this plugin's execute return\n * (capability-runtime-guards normalization): a `TodoState`, optionally with a\n * response-only `warning`. Anything else — a JSON.parse failure or a\n * different shape — returns undefined so the renderer falls back to the raw\n * text.\n */\nfunction parseTodoResultText(text: string): { state: TodoState; warning?: string } | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as { todos?: unknown; warning?: unknown };\n\tif (!Array.isArray(record.todos)) return undefined;\n\tif (record.warning !== undefined && typeof record.warning !== \"string\") return undefined;\n\tconst state: TodoState = { todos: record.todos as TodoRecord[] };\n\treturn record.warning === undefined ? { state } : { state, warning: record.warning };\n}\n\n/**\n * renderResult for todo_update: re-renders the tool's own state payload as\n * the readable listing instead of the host's generic one-line JSON fallback.\n * Collapsed blocks show the summary and record lines only; expanded blocks\n * add the evidence lines; a response `warning` rides along as a final\n * warning-colored line. Non-state payloads and malformed records render\n * verbatim as toolOutput.\n */\nfunction renderTodoResult(\n\tresult: ToolResultLike,\n\toptions: RenderResultOptionsLike,\n\ttheme: ThemeLike,\n\tcontext: RenderContextLike,\n): TodoResultComponent | undefined {\n\tconst text = result.content.find((part) => part.type === \"text\")?.text;\n\tif (text === undefined || text === \"\") return undefined;\n\tconst parsed = parseTodoResultText(text);\n\tlet listing: string;\n\tif (parsed) {\n\t\t// The parse gate is envelope-level, so a parsed state may still hold\n\t\t// malformed records (null elements, a record missing evidence).\n\t\t// Rendering those must not throw out of the plugin: fall back to the\n\t\t// raw text, the same path as a parse failure.\n\t\ttry {\n\t\t\tlisting = renderTodoState(parsed.state, { evidence: options.expanded });\n\t\t\tif (parsed.warning !== undefined) {\n\t\t\t\t// Fold newlines like the record fields so the warning stays one line.\n\t\t\t\tlisting = `${listing}\\nwarning: ${parsed.warning.replace(/\\n/g, \" \")}`;\n\t\t\t}\n\t\t} catch {\n\t\t\tlisting = text;\n\t\t}\n\t} else {\n\t\tlisting = text;\n\t}\n\t// Reuse the host-provided component for this render slot (the ls.ts\n\t// precedent): same instance in, same instance out, with fresh text.\n\tif (context.lastComponent instanceof TodoResultComponent) {\n\t\tcontext.lastComponent.setText(listing);\n\t\treturn context.lastComponent;\n\t}\n\treturn new TodoResultComponent(listing, theme);\n}\n\n/**\n * Whether the session branch's LATEST `plan.state` snapshot knows a plan with\n * the given id (the same latest-snapshot semantics as {@link loadState} and\n * the same advisory pattern as the plan plugin's goalKnownToSession). The\n * check is advisory only (#10): a missing plan downgrades to a warning in the\n * tool response — the soft-reference write path never fails on it.\n */\nfunction planKnownToSession(api: CapabilityAPI, planId: string): boolean {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.customType !== planStateEntry) continue;\n\t\tconst data = entry.data;\n\t\tif (!data || typeof data !== \"object\" || Array.isArray(data)) return false;\n\t\tconst plans = (data as { plans?: unknown }).plans;\n\t\tif (!Array.isArray(plans)) return false;\n\t\treturn plans.some(\n\t\t\t(plan) => plan !== null && typeof plan === \"object\" && (plan as { id?: unknown }).id === planId,\n\t\t);\n\t}\n\treturn false;\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n): Promise<readonly DisposableRegistration[]> | readonly DisposableRegistration[] {\n\tlet state = loadState(api);\n\tconst refreshState = (): TodoState => {\n\t\tif (api.session) state = loadState(api);\n\t\treturn state;\n\t};\n\n\tconst persist = async (): Promise<TodoState> => {\n\t\tapi.session?.appendEntry(stateEntry, state);\n\t\tawait api.publish({ type: \"todo.changed\", version: 1, correlationId: stateEntry, data: state });\n\t\treturn state;\n\t};\n\n\tconst tool = api.registerTool({\n\t\tname: \"todo_update\",\n\t\tlabel: \"Update todo\",\n\t\tdescription: \"Update a todo that softly references a plan by id\",\n\t\tparameters: todoSchema,\n\t\trenderResult: renderTodoResult,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\trefreshState();\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst id = text(input.id, \"id\");\n\t\t\tconst planId = text(input.planId, \"planId\");\n\t\t\tconst title = text(input.title, \"title\");\n\t\t\tconst nextRevision = revision(input.revision);\n\t\t\tconst current = state.todos.find((item) => item.id === id);\n\t\t\tif (current && nextRevision <= current.revision) throw new Error(`stale todo revision: ${id}`);\n\t\t\tif (current?.status === \"done\") throw new Error(`todo is terminal and cannot be updated: ${id}`);\n\t\t\tconst status =\n\t\t\t\tinput.status === undefined\n\t\t\t\t\t? (current?.status ?? \"pending\")\n\t\t\t\t\t: (text(input.status, \"status\") as TodoRecord[\"status\"]);\n\t\t\tif (![\"pending\", \"done\", \"blocked\"].includes(status)) throw new Error(\"invalid todo status\");\n\t\t\tconst values = evidence(input.evidence);\n\t\t\tif (status === \"done\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo completion requires at least one evidence reference\");\n\t\t\t}\n\t\t\tconst record: TodoRecord = { id, planId, title, status, revision: nextRevision, evidence: values };\n\t\t\tstate = { ...state, todos: [...state.todos.filter((item) => item.id !== id), record] };\n\t\t\tconst saved = await persist();\n\t\t\tif (planKnownToSession(api, planId)) return saved;\n\t\t\treturn {\n\t\t\t\t...saved,\n\t\t\t\twarning: `planId \"${planId}\" does not match any plan in this session's plan state; the todo keeps the soft reference, but the plan is unknown`,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst command = api.registerCommand({\n\t\tname: \"todo\",\n\t\tdescription: \"Show this session's todos (status and plan reference)\",\n\t\texecute: async (args: string) => {\n\t\t\ttry {\n\t\t\t\tif (args.trim() !== \"\") return { status: \"error\" as const, message: \"Usage: /todo\" };\n\t\t\t\treturn { status: \"success\" as const, message: renderTodoState(refreshState()) };\n\t\t\t} catch (error) {\n\t\t\t\t// A slash command must not crash the host (memory /promote precedent).\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\treturn [tool, command];\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGrF,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;CACpB;AA+DD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAwB1F;AAyMD,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,aAAa,GAChB,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC,GAAG,SAAS,sBAAsB,EAAE,CAsEhF","sourcesContent":["/**\n * Todo-set capability plugin: a self-contained todo state machine, extracted\n * from the host's `src/capabilities/todo-set.ts` (D-075 S4, first B-class\n * batch).\n *\n * Todos persist as append-only `todo.state` session entries and every change\n * is announced as a `todo.changed` event. `planId` is a soft reference: a\n * plain string whose target is checked ADVISORILY against the plan plugin's\n * LATEST `plan.state` snapshot (the same latest-snapshot semantics as this\n * plugin's own {@link loadState}); an unknown id yields a response warning,\n * never a failure (the same #10 pattern as the plan plugin's goalId check).\n * The plugin only consumes the public `session`, `events`, and `tools`\n * capabilities and never writes state owned by another plugin.\n */\n\nimport type { CapabilityAPI, DisposableRegistration } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\n\nexport interface TodoRecord {\n\tid: string;\n\t/** Soft reference: a plain string; unknown ids yield a response warning, not a failure. */\n\tplanId: string;\n\ttitle: string;\n\t/**\n\t * Lifecycle status. `cancelled` is terminal (voided: permanently retired,\n\t * no longer part of execution) and shares done's no-further-update guard.\n\t */\n\tstatus: \"pending\" | \"done\" | \"blocked\" | \"cancelled\";\n\trevision: number;\n\tevidence: string[];\n}\n\nexport interface TodoState {\n\ttodos: TodoRecord[];\n}\n\nconst stateEntry = \"todo.state\";\n/** Session protocol key of the plan plugin's snapshot entry (plugins/plan). */\nconst planStateEntry = \"plan.state\";\n\nconst todoSchema = Type.Object({\n\tid: Type.String({ description: \"Stable id of the todo to create or update\" }),\n\tplanId: Type.String({\n\t\tdescription:\n\t\t\t\"Soft reference to a plan id; an id unknown to this session's plan state yields a response warning, not a failure\",\n\t}),\n\ttitle: Type.String({ description: \"Human-readable todo title\" }),\n\trevision: Type.Integer({\n\t\tdescription: \"New revision counter; must be greater than the stored revision of an existing todo\",\n\t\tminimum: 0,\n\t}),\n\tstatus: Type.Optional(\n\t\tType.Union([Type.Literal(\"pending\"), Type.Literal(\"done\"), Type.Literal(\"blocked\"), Type.Literal(\"cancelled\")], {\n\t\t\tdescription:\n\t\t\t\t\"Next status; defaults to the stored status for existing todos, otherwise pending. done and cancelled are terminal: once a todo holds either status it can never be updated again, so finish every evidence reference before marking it done, and never mark done to avoid reporting unfinished work. Use blocked only for temporarily stuck work that may resume; use cancelled when the user retires or skips the todo\",\n\t\t}),\n\t),\n\tevidence: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Final evidence references; at least one is required when status is done or cancelled (for cancelled, cite the user's words that retired the todo). The list is fixed at that moment — a terminal todo cannot be updated to add later evidence such as a commit id\",\n\t\t}),\n\t),\n});\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Todo input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction text(value: unknown, name: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${name} must be a non-empty string`);\n\treturn value;\n}\n\nfunction revision(value: unknown): number {\n\tif (!Number.isInteger(value) || (value as number) < 0) throw new Error(\"revision must be a non-negative integer\");\n\treturn value as number;\n}\n\nfunction evidence(value: unknown): string[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value) || value.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new Error(\"evidence must be a string array\");\n\t}\n\treturn [...(value as string[])];\n}\n\nfunction loadState(api: CapabilityAPI): TodoState {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tconst entry = [...entries].reverse().find((item) => item.customType === stateEntry);\n\tif (!entry?.data || typeof entry.data !== \"object\") return { todos: [] };\n\tconst data = entry.data as Partial<TodoState>;\n\treturn { todos: Array.isArray(data.todos) ? [...data.todos] : [] };\n}\n\n/**\n * `/todo` command text rendering: one line per todo (`id [status → planId]\n * title`) plus the evidence lines that justified completion. The leading count\n * line lists its non-zero segments in done → blocked → cancelled order. Plain\n * text only —\n * every host shape can render it, no TUI required (the same L1 contract as\n * the workflow plugin's `/workflow`). Collapsed tool blocks pass\n * `{ evidence: false }` to omit the evidence lines; the default keeps the\n * command output byte-for-byte stable.\n */\nexport function renderTodoState(state: TodoState, options?: { evidence?: boolean }): string {\n\tconst withEvidence = options?.evidence ?? true;\n\tif (state.todos.length === 0) return \"no todos in this session; create one with todo_update\";\n\tconst done = state.todos.filter((todo) => todo.status === \"done\").length;\n\tconst blocked = state.todos.filter((todo) => todo.status === \"blocked\").length;\n\tconst cancelled = state.todos.filter((todo) => todo.status === \"cancelled\").length;\n\t// Count segments in the fixed done → blocked → cancelled order; zero\n\t// segments are omitted (the rule blocked always had), so a total with no\n\t// countable status renders bare instead of with empty parentheses.\n\tconst segments: string[] = [];\n\tif (done > 0) segments.push(`${done} done`);\n\tif (blocked > 0) segments.push(`${blocked} blocked`);\n\tif (cancelled > 0) segments.push(`${cancelled} cancelled`);\n\tconst lines = [\n\t\tsegments.length > 0 ? `todos: ${state.todos.length} (${segments.join(\", \")})` : `todos: ${state.todos.length}`,\n\t];\n\t// Record fields only validate non-emptiness, so fold newlines to keep the\n\t// one-line-per-record shape whatever the model wrote.\n\tconst inline = (value: string): string => value.replace(/\\n/g, \" \");\n\tfor (const todo of state.todos) {\n\t\tlines.push(`- ${inline(todo.id)} [${todo.status} → ${inline(todo.planId)}] ${inline(todo.title)}`);\n\t\tif (withEvidence) for (const item of todo.evidence) lines.push(` evidence: ${inline(item)}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Structural slices of the host tool-render contracts the renderer needs.\n * Declared locally on purpose: this plugin must stay self-contained and must\n * not import `@agent-forge/tui` (the tool-definition.ts structural hosting\n * boundary), so renderResult talks to these shapes instead of the real\n * Theme/Component types.\n */\ninterface ToolResultTextPartLike {\n\treadonly type: string;\n\treadonly text?: string;\n}\n\ninterface ToolResultLike {\n\treadonly content: readonly ToolResultTextPartLike[];\n}\n\ninterface RenderResultOptionsLike {\n\treadonly expanded: boolean;\n\treadonly isPartial: boolean;\n}\n\ninterface ThemeLike {\n\tfg(color: string, text: string): string;\n}\n\ninterface RenderContextLike {\n\treadonly lastComponent?: unknown;\n}\n\n/**\n * Simplified East Asian width table: these ranges cover the CJK blocks state\n * titles realistically use and count as 2 columns each; every other code\n * point counts as 1. Not a full wcwidth — combining marks and ambiguous\n * widths fall back to 1, the safe (narrower) default for row folding.\n */\nfunction isWideCodePoint(code: number): boolean {\n\treturn (\n\t\t(code >= 0x1100 && code <= 0x115f) ||\n\t\t(code >= 0x2e80 && code <= 0xa4cf) ||\n\t\t(code >= 0xac00 && code <= 0xd7a3) ||\n\t\t(code >= 0xf900 && code <= 0xfaff) ||\n\t\t(code >= 0xfe30 && code <= 0xfe4f) ||\n\t\t(code >= 0xff00 && code <= 0xff60) ||\n\t\t(code >= 0xffe0 && code <= 0xffe6) ||\n\t\t(code >= 0x20000 && code <= 0x3fffd)\n\t);\n}\n\n/** Folds one logical line into rows of at most `width` display columns. */\nfunction wrapLine(line: string, width: number): string[] {\n\tif (width <= 0) return [line];\n\tconst rows: string[] = [];\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of line) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (row !== \"\" && columns + cost > width) {\n\t\t\trows.push(row);\n\t\t\trow = character;\n\t\t\tcolumns = cost;\n\t\t} else {\n\t\t\trow += character;\n\t\t\tcolumns += cost;\n\t\t}\n\t}\n\trows.push(row);\n\treturn rows;\n}\n\n/**\n * Structural TUI component rendering a todo_update result as the plain state\n * listing: `warning: ` lines take the host theme's warning color, every other\n * row toolOutput (color is decided per logical line, so a wrapped warning\n * stays warning-colored). Rows are cached per width — the host may render the\n * same result repeatedly at one width — and dropped on text updates or\n * invalidate.\n */\nclass TodoResultComponent {\n\tprivate text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly rowsByWidth = new Map<number, string[]>();\n\n\tconstructor(text: string, theme: ThemeLike) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t}\n\n\tsetText(text: string): void {\n\t\tif (text === this.text) return;\n\t\tthis.text = text;\n\t\tthis.rowsByWidth.clear();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst cached = this.rowsByWidth.get(width);\n\t\tif (cached) return cached;\n\t\tconst rows: string[] = [];\n\t\tfor (const line of this.text.split(\"\\n\")) {\n\t\t\tconst color = line.startsWith(\"warning: \") ? \"warning\" : \"toolOutput\";\n\t\t\tfor (const row of wrapLine(line, width)) rows.push(this.theme.fg(color, row));\n\t\t}\n\t\tthis.rowsByWidth.set(width, rows);\n\t\treturn rows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.rowsByWidth.clear();\n\t}\n}\n\n/**\n * Parses the text part the host serialized from this plugin's execute return\n * (capability-runtime-guards normalization): a `TodoState`, optionally with a\n * response-only `warning`. Anything else — a JSON.parse failure or a\n * different shape — returns undefined so the renderer falls back to the raw\n * text.\n */\nfunction parseTodoResultText(text: string): { state: TodoState; warning?: string } | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as { todos?: unknown; warning?: unknown };\n\tif (!Array.isArray(record.todos)) return undefined;\n\tif (record.warning !== undefined && typeof record.warning !== \"string\") return undefined;\n\tconst state: TodoState = { todos: record.todos as TodoRecord[] };\n\treturn record.warning === undefined ? { state } : { state, warning: record.warning };\n}\n\n/**\n * renderResult for todo_update: re-renders the tool's own state payload as\n * the readable listing instead of the host's generic one-line JSON fallback.\n * Collapsed blocks show the summary and record lines only; expanded blocks\n * add the evidence lines; a response `warning` rides along as a final\n * warning-colored line. Non-state payloads and malformed records render\n * verbatim as toolOutput.\n */\nfunction renderTodoResult(\n\tresult: ToolResultLike,\n\toptions: RenderResultOptionsLike,\n\ttheme: ThemeLike,\n\tcontext: RenderContextLike,\n): TodoResultComponent | undefined {\n\tconst text = result.content.find((part) => part.type === \"text\")?.text;\n\tif (text === undefined || text === \"\") return undefined;\n\tconst parsed = parseTodoResultText(text);\n\tlet listing: string;\n\tif (parsed) {\n\t\t// The parse gate is envelope-level, so a parsed state may still hold\n\t\t// malformed records (null elements, a record missing evidence).\n\t\t// Rendering those must not throw out of the plugin: fall back to the\n\t\t// raw text, the same path as a parse failure.\n\t\ttry {\n\t\t\tlisting = renderTodoState(parsed.state, { evidence: options.expanded });\n\t\t\tif (parsed.warning !== undefined) {\n\t\t\t\t// Fold newlines like the record fields so the warning stays one line.\n\t\t\t\tlisting = `${listing}\\nwarning: ${parsed.warning.replace(/\\n/g, \" \")}`;\n\t\t\t}\n\t\t} catch {\n\t\t\tlisting = text;\n\t\t}\n\t} else {\n\t\tlisting = text;\n\t}\n\t// Reuse the host-provided component for this render slot (the ls.ts\n\t// precedent): same instance in, same instance out, with fresh text.\n\tif (context.lastComponent instanceof TodoResultComponent) {\n\t\tcontext.lastComponent.setText(listing);\n\t\treturn context.lastComponent;\n\t}\n\treturn new TodoResultComponent(listing, theme);\n}\n\n/**\n * Whether the session branch's LATEST `plan.state` snapshot knows a plan with\n * the given id (the same latest-snapshot semantics as {@link loadState} and\n * the same advisory pattern as the plan plugin's goalKnownToSession). The\n * check is advisory only (#10): a missing plan downgrades to a warning in the\n * tool response — the soft-reference write path never fails on it.\n */\nfunction planKnownToSession(api: CapabilityAPI, planId: string): boolean {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.customType !== planStateEntry) continue;\n\t\tconst data = entry.data;\n\t\tif (!data || typeof data !== \"object\" || Array.isArray(data)) return false;\n\t\tconst plans = (data as { plans?: unknown }).plans;\n\t\tif (!Array.isArray(plans)) return false;\n\t\treturn plans.some(\n\t\t\t(plan) => plan !== null && typeof plan === \"object\" && (plan as { id?: unknown }).id === planId,\n\t\t);\n\t}\n\treturn false;\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n): Promise<readonly DisposableRegistration[]> | readonly DisposableRegistration[] {\n\tlet state = loadState(api);\n\tconst refreshState = (): TodoState => {\n\t\tif (api.session) state = loadState(api);\n\t\treturn state;\n\t};\n\n\tconst persist = async (): Promise<TodoState> => {\n\t\tapi.session?.appendEntry(stateEntry, state);\n\t\tawait api.publish({ type: \"todo.changed\", version: 1, correlationId: stateEntry, data: state });\n\t\treturn state;\n\t};\n\n\tconst tool = api.registerTool({\n\t\tname: \"todo_update\",\n\t\tlabel: \"Update todo\",\n\t\tdescription:\n\t\t\t\"Update a todo that softly references a plan by id. Create with status pending at revision 1, update unfinished items with a higher revision; marking done or cancelled is final and requires evidence, so commit the work first and then complete the todo\",\n\t\tparameters: todoSchema,\n\t\trenderResult: renderTodoResult,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\trefreshState();\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst id = text(input.id, \"id\");\n\t\t\tconst planId = text(input.planId, \"planId\");\n\t\t\tconst title = text(input.title, \"title\");\n\t\t\tconst nextRevision = revision(input.revision);\n\t\t\tconst current = state.todos.find((item) => item.id === id);\n\t\t\tif (current && nextRevision <= current.revision) throw new Error(`stale todo revision: ${id}`);\n\t\t\tif (current?.status === \"done\" || current?.status === \"cancelled\") {\n\t\t\t\tthrow new Error(`todo is terminal and cannot be updated: ${id}`);\n\t\t\t}\n\t\t\tconst status =\n\t\t\t\tinput.status === undefined\n\t\t\t\t\t? (current?.status ?? \"pending\")\n\t\t\t\t\t: (text(input.status, \"status\") as TodoRecord[\"status\"]);\n\t\t\tif (![\"pending\", \"done\", \"blocked\", \"cancelled\"].includes(status)) throw new Error(\"invalid todo status\");\n\t\t\tconst values = evidence(input.evidence);\n\t\t\tif (status === \"done\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo completion requires at least one evidence reference\");\n\t\t\t}\n\t\t\tif (status === \"cancelled\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo cancellation requires at least one evidence reference\");\n\t\t\t}\n\t\t\tconst record: TodoRecord = { id, planId, title, status, revision: nextRevision, evidence: values };\n\t\t\tstate = { ...state, todos: [...state.todos.filter((item) => item.id !== id), record] };\n\t\t\tconst saved = await persist();\n\t\t\tif (planKnownToSession(api, planId)) return saved;\n\t\t\treturn {\n\t\t\t\t...saved,\n\t\t\t\twarning: `planId \"${planId}\" does not match any plan in this session's plan state; the todo keeps the soft reference, but the plan is unknown`,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst command = api.registerCommand({\n\t\tname: \"todo\",\n\t\tdescription: \"Show this session's todos (status and plan reference)\",\n\t\texecute: async (args: string) => {\n\t\t\ttry {\n\t\t\t\tif (args.trim() !== \"\") return { status: \"error\" as const, message: \"Usage: /todo\" };\n\t\t\t\treturn { status: \"success\" as const, message: renderTodoState(refreshState()) };\n\t\t\t} catch (error) {\n\t\t\t\t// A slash command must not crash the host (memory /promote precedent).\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\treturn [tool, command];\n}\n"]}
package/dist/index.js CHANGED
@@ -26,11 +26,11 @@ const todoSchema = Type.Object({
26
26
  description: "New revision counter; must be greater than the stored revision of an existing todo",
27
27
  minimum: 0,
28
28
  }),
29
- status: Type.Optional(Type.Union([Type.Literal("pending"), Type.Literal("done"), Type.Literal("blocked")], {
30
- description: "Next status; defaults to the stored status for existing todos, otherwise pending",
29
+ status: Type.Optional(Type.Union([Type.Literal("pending"), Type.Literal("done"), Type.Literal("blocked"), Type.Literal("cancelled")], {
30
+ description: "Next status; defaults to the stored status for existing todos, otherwise pending. done and cancelled are terminal: once a todo holds either status it can never be updated again, so finish every evidence reference before marking it done, and never mark done to avoid reporting unfinished work. Use blocked only for temporarily stuck work that may resume; use cancelled when the user retires or skips the todo",
31
31
  })),
32
32
  evidence: Type.Optional(Type.Array(Type.String(), {
33
- description: "Evidence references; at least one is required when status is done",
33
+ description: "Final evidence references; at least one is required when status is done or cancelled (for cancelled, cite the user's words that retired the todo). The list is fixed at that moment — a terminal todo cannot be updated to add later evidence such as a commit id",
34
34
  })),
35
35
  });
36
36
  function objectInput(first, second) {
@@ -67,7 +67,9 @@ function loadState(api) {
67
67
  }
68
68
  /**
69
69
  * `/todo` command text rendering: one line per todo (`id [status → planId]
70
- * title`) plus the evidence lines that justified completion. Plain text only —
70
+ * title`) plus the evidence lines that justified completion. The leading count
71
+ * line lists its non-zero segments in done → blocked → cancelled order. Plain
72
+ * text only —
71
73
  * every host shape can render it, no TUI required (the same L1 contract as
72
74
  * the workflow plugin's `/workflow`). Collapsed tool blocks pass
73
75
  * `{ evidence: false }` to omit the evidence lines; the default keeps the
@@ -79,8 +81,20 @@ export function renderTodoState(state, options) {
79
81
  return "no todos in this session; create one with todo_update";
80
82
  const done = state.todos.filter((todo) => todo.status === "done").length;
81
83
  const blocked = state.todos.filter((todo) => todo.status === "blocked").length;
82
- const counts = blocked > 0 ? `${done} done, ${blocked} blocked` : `${done} done`;
83
- const lines = [`todos: ${state.todos.length} (${counts})`];
84
+ const cancelled = state.todos.filter((todo) => todo.status === "cancelled").length;
85
+ // Count segments in the fixed done → blocked → cancelled order; zero
86
+ // segments are omitted (the rule blocked always had), so a total with no
87
+ // countable status renders bare instead of with empty parentheses.
88
+ const segments = [];
89
+ if (done > 0)
90
+ segments.push(`${done} done`);
91
+ if (blocked > 0)
92
+ segments.push(`${blocked} blocked`);
93
+ if (cancelled > 0)
94
+ segments.push(`${cancelled} cancelled`);
95
+ const lines = [
96
+ segments.length > 0 ? `todos: ${state.todos.length} (${segments.join(", ")})` : `todos: ${state.todos.length}`,
97
+ ];
84
98
  // Record fields only validate non-emptiness, so fold newlines to keep the
85
99
  // one-line-per-record shape whatever the model wrote.
86
100
  const inline = (value) => value.replace(/\n/g, " ");
@@ -273,7 +287,7 @@ export function createCapability(api) {
273
287
  const tool = api.registerTool({
274
288
  name: "todo_update",
275
289
  label: "Update todo",
276
- description: "Update a todo that softly references a plan by id",
290
+ description: "Update a todo that softly references a plan by id. Create with status pending at revision 1, update unfinished items with a higher revision; marking done or cancelled is final and requires evidence, so commit the work first and then complete the todo",
277
291
  parameters: todoSchema,
278
292
  renderResult: renderTodoResult,
279
293
  execute: async (first, second) => {
@@ -286,17 +300,21 @@ export function createCapability(api) {
286
300
  const current = state.todos.find((item) => item.id === id);
287
301
  if (current && nextRevision <= current.revision)
288
302
  throw new Error(`stale todo revision: ${id}`);
289
- if (current?.status === "done")
303
+ if (current?.status === "done" || current?.status === "cancelled") {
290
304
  throw new Error(`todo is terminal and cannot be updated: ${id}`);
305
+ }
291
306
  const status = input.status === undefined
292
307
  ? (current?.status ?? "pending")
293
308
  : text(input.status, "status");
294
- if (!["pending", "done", "blocked"].includes(status))
309
+ if (!["pending", "done", "blocked", "cancelled"].includes(status))
295
310
  throw new Error("invalid todo status");
296
311
  const values = evidence(input.evidence);
297
312
  if (status === "done" && values.length === 0) {
298
313
  throw new Error("todo completion requires at least one evidence reference");
299
314
  }
315
+ if (status === "cancelled" && values.length === 0) {
316
+ throw new Error("todo cancellation requires at least one evidence reference");
317
+ }
300
318
  const record = { id, planId, title, status, revision: nextRevision, evidence: values };
301
319
  state = { ...state, todos: [...state.todos.filter((item) => item.id !== id), record] };
302
320
  const saved = await persist();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Todo-set capability plugin: a self-contained todo state machine, extracted\n * from the host's `src/capabilities/todo-set.ts` (D-075 S4, first B-class\n * batch).\n *\n * Todos persist as append-only `todo.state` session entries and every change\n * is announced as a `todo.changed` event. `planId` is a soft reference: a\n * plain string whose target is checked ADVISORILY against the plan plugin's\n * LATEST `plan.state` snapshot (the same latest-snapshot semantics as this\n * plugin's own {@link loadState}); an unknown id yields a response warning,\n * never a failure (the same #10 pattern as the plan plugin's goalId check).\n * The plugin only consumes the public `session`, `events`, and `tools`\n * capabilities and never writes state owned by another plugin.\n */\n\nimport type { CapabilityAPI, DisposableRegistration } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\n\nexport interface TodoRecord {\n\tid: string;\n\t/** Soft reference: a plain string; unknown ids yield a response warning, not a failure. */\n\tplanId: string;\n\ttitle: string;\n\tstatus: \"pending\" | \"done\" | \"blocked\";\n\trevision: number;\n\tevidence: string[];\n}\n\nexport interface TodoState {\n\ttodos: TodoRecord[];\n}\n\nconst stateEntry = \"todo.state\";\n/** Session protocol key of the plan plugin's snapshot entry (plugins/plan). */\nconst planStateEntry = \"plan.state\";\n\nconst todoSchema = Type.Object({\n\tid: Type.String({ description: \"Stable id of the todo to create or update\" }),\n\tplanId: Type.String({\n\t\tdescription:\n\t\t\t\"Soft reference to a plan id; an id unknown to this session's plan state yields a response warning, not a failure\",\n\t}),\n\ttitle: Type.String({ description: \"Human-readable todo title\" }),\n\trevision: Type.Integer({\n\t\tdescription: \"New revision counter; must be greater than the stored revision of an existing todo\",\n\t\tminimum: 0,\n\t}),\n\tstatus: Type.Optional(\n\t\tType.Union([Type.Literal(\"pending\"), Type.Literal(\"done\"), Type.Literal(\"blocked\")], {\n\t\t\tdescription: \"Next status; defaults to the stored status for existing todos, otherwise pending\",\n\t\t}),\n\t),\n\tevidence: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription: \"Evidence references; at least one is required when status is done\",\n\t\t}),\n\t),\n});\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Todo input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction text(value: unknown, name: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${name} must be a non-empty string`);\n\treturn value;\n}\n\nfunction revision(value: unknown): number {\n\tif (!Number.isInteger(value) || (value as number) < 0) throw new Error(\"revision must be a non-negative integer\");\n\treturn value as number;\n}\n\nfunction evidence(value: unknown): string[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value) || value.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new Error(\"evidence must be a string array\");\n\t}\n\treturn [...(value as string[])];\n}\n\nfunction loadState(api: CapabilityAPI): TodoState {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tconst entry = [...entries].reverse().find((item) => item.customType === stateEntry);\n\tif (!entry?.data || typeof entry.data !== \"object\") return { todos: [] };\n\tconst data = entry.data as Partial<TodoState>;\n\treturn { todos: Array.isArray(data.todos) ? [...data.todos] : [] };\n}\n\n/**\n * `/todo` command text rendering: one line per todo (`id [status → planId]\n * title`) plus the evidence lines that justified completion. Plain text only —\n * every host shape can render it, no TUI required (the same L1 contract as\n * the workflow plugin's `/workflow`). Collapsed tool blocks pass\n * `{ evidence: false }` to omit the evidence lines; the default keeps the\n * command output byte-for-byte stable.\n */\nexport function renderTodoState(state: TodoState, options?: { evidence?: boolean }): string {\n\tconst withEvidence = options?.evidence ?? true;\n\tif (state.todos.length === 0) return \"no todos in this session; create one with todo_update\";\n\tconst done = state.todos.filter((todo) => todo.status === \"done\").length;\n\tconst blocked = state.todos.filter((todo) => todo.status === \"blocked\").length;\n\tconst counts = blocked > 0 ? `${done} done, ${blocked} blocked` : `${done} done`;\n\tconst lines = [`todos: ${state.todos.length} (${counts})`];\n\t// Record fields only validate non-emptiness, so fold newlines to keep the\n\t// one-line-per-record shape whatever the model wrote.\n\tconst inline = (value: string): string => value.replace(/\\n/g, \" \");\n\tfor (const todo of state.todos) {\n\t\tlines.push(`- ${inline(todo.id)} [${todo.status} → ${inline(todo.planId)}] ${inline(todo.title)}`);\n\t\tif (withEvidence) for (const item of todo.evidence) lines.push(` evidence: ${inline(item)}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Structural slices of the host tool-render contracts the renderer needs.\n * Declared locally on purpose: this plugin must stay self-contained and must\n * not import `@agent-forge/tui` (the tool-definition.ts structural hosting\n * boundary), so renderResult talks to these shapes instead of the real\n * Theme/Component types.\n */\ninterface ToolResultTextPartLike {\n\treadonly type: string;\n\treadonly text?: string;\n}\n\ninterface ToolResultLike {\n\treadonly content: readonly ToolResultTextPartLike[];\n}\n\ninterface RenderResultOptionsLike {\n\treadonly expanded: boolean;\n\treadonly isPartial: boolean;\n}\n\ninterface ThemeLike {\n\tfg(color: string, text: string): string;\n}\n\ninterface RenderContextLike {\n\treadonly lastComponent?: unknown;\n}\n\n/**\n * Simplified East Asian width table: these ranges cover the CJK blocks state\n * titles realistically use and count as 2 columns each; every other code\n * point counts as 1. Not a full wcwidth — combining marks and ambiguous\n * widths fall back to 1, the safe (narrower) default for row folding.\n */\nfunction isWideCodePoint(code: number): boolean {\n\treturn (\n\t\t(code >= 0x1100 && code <= 0x115f) ||\n\t\t(code >= 0x2e80 && code <= 0xa4cf) ||\n\t\t(code >= 0xac00 && code <= 0xd7a3) ||\n\t\t(code >= 0xf900 && code <= 0xfaff) ||\n\t\t(code >= 0xfe30 && code <= 0xfe4f) ||\n\t\t(code >= 0xff00 && code <= 0xff60) ||\n\t\t(code >= 0xffe0 && code <= 0xffe6) ||\n\t\t(code >= 0x20000 && code <= 0x3fffd)\n\t);\n}\n\n/** Folds one logical line into rows of at most `width` display columns. */\nfunction wrapLine(line: string, width: number): string[] {\n\tif (width <= 0) return [line];\n\tconst rows: string[] = [];\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of line) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (row !== \"\" && columns + cost > width) {\n\t\t\trows.push(row);\n\t\t\trow = character;\n\t\t\tcolumns = cost;\n\t\t} else {\n\t\t\trow += character;\n\t\t\tcolumns += cost;\n\t\t}\n\t}\n\trows.push(row);\n\treturn rows;\n}\n\n/**\n * Structural TUI component rendering a todo_update result as the plain state\n * listing: `warning: ` lines take the host theme's warning color, every other\n * row toolOutput (color is decided per logical line, so a wrapped warning\n * stays warning-colored). Rows are cached per width — the host may render the\n * same result repeatedly at one width — and dropped on text updates or\n * invalidate.\n */\nclass TodoResultComponent {\n\tprivate text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly rowsByWidth = new Map<number, string[]>();\n\n\tconstructor(text: string, theme: ThemeLike) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t}\n\n\tsetText(text: string): void {\n\t\tif (text === this.text) return;\n\t\tthis.text = text;\n\t\tthis.rowsByWidth.clear();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst cached = this.rowsByWidth.get(width);\n\t\tif (cached) return cached;\n\t\tconst rows: string[] = [];\n\t\tfor (const line of this.text.split(\"\\n\")) {\n\t\t\tconst color = line.startsWith(\"warning: \") ? \"warning\" : \"toolOutput\";\n\t\t\tfor (const row of wrapLine(line, width)) rows.push(this.theme.fg(color, row));\n\t\t}\n\t\tthis.rowsByWidth.set(width, rows);\n\t\treturn rows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.rowsByWidth.clear();\n\t}\n}\n\n/**\n * Parses the text part the host serialized from this plugin's execute return\n * (capability-runtime-guards normalization): a `TodoState`, optionally with a\n * response-only `warning`. Anything else — a JSON.parse failure or a\n * different shape — returns undefined so the renderer falls back to the raw\n * text.\n */\nfunction parseTodoResultText(text: string): { state: TodoState; warning?: string } | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as { todos?: unknown; warning?: unknown };\n\tif (!Array.isArray(record.todos)) return undefined;\n\tif (record.warning !== undefined && typeof record.warning !== \"string\") return undefined;\n\tconst state: TodoState = { todos: record.todos as TodoRecord[] };\n\treturn record.warning === undefined ? { state } : { state, warning: record.warning };\n}\n\n/**\n * renderResult for todo_update: re-renders the tool's own state payload as\n * the readable listing instead of the host's generic one-line JSON fallback.\n * Collapsed blocks show the summary and record lines only; expanded blocks\n * add the evidence lines; a response `warning` rides along as a final\n * warning-colored line. Non-state payloads and malformed records render\n * verbatim as toolOutput.\n */\nfunction renderTodoResult(\n\tresult: ToolResultLike,\n\toptions: RenderResultOptionsLike,\n\ttheme: ThemeLike,\n\tcontext: RenderContextLike,\n): TodoResultComponent | undefined {\n\tconst text = result.content.find((part) => part.type === \"text\")?.text;\n\tif (text === undefined || text === \"\") return undefined;\n\tconst parsed = parseTodoResultText(text);\n\tlet listing: string;\n\tif (parsed) {\n\t\t// The parse gate is envelope-level, so a parsed state may still hold\n\t\t// malformed records (null elements, a record missing evidence).\n\t\t// Rendering those must not throw out of the plugin: fall back to the\n\t\t// raw text, the same path as a parse failure.\n\t\ttry {\n\t\t\tlisting = renderTodoState(parsed.state, { evidence: options.expanded });\n\t\t\tif (parsed.warning !== undefined) {\n\t\t\t\t// Fold newlines like the record fields so the warning stays one line.\n\t\t\t\tlisting = `${listing}\\nwarning: ${parsed.warning.replace(/\\n/g, \" \")}`;\n\t\t\t}\n\t\t} catch {\n\t\t\tlisting = text;\n\t\t}\n\t} else {\n\t\tlisting = text;\n\t}\n\t// Reuse the host-provided component for this render slot (the ls.ts\n\t// precedent): same instance in, same instance out, with fresh text.\n\tif (context.lastComponent instanceof TodoResultComponent) {\n\t\tcontext.lastComponent.setText(listing);\n\t\treturn context.lastComponent;\n\t}\n\treturn new TodoResultComponent(listing, theme);\n}\n\n/**\n * Whether the session branch's LATEST `plan.state` snapshot knows a plan with\n * the given id (the same latest-snapshot semantics as {@link loadState} and\n * the same advisory pattern as the plan plugin's goalKnownToSession). The\n * check is advisory only (#10): a missing plan downgrades to a warning in the\n * tool response — the soft-reference write path never fails on it.\n */\nfunction planKnownToSession(api: CapabilityAPI, planId: string): boolean {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.customType !== planStateEntry) continue;\n\t\tconst data = entry.data;\n\t\tif (!data || typeof data !== \"object\" || Array.isArray(data)) return false;\n\t\tconst plans = (data as { plans?: unknown }).plans;\n\t\tif (!Array.isArray(plans)) return false;\n\t\treturn plans.some(\n\t\t\t(plan) => plan !== null && typeof plan === \"object\" && (plan as { id?: unknown }).id === planId,\n\t\t);\n\t}\n\treturn false;\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n): Promise<readonly DisposableRegistration[]> | readonly DisposableRegistration[] {\n\tlet state = loadState(api);\n\tconst refreshState = (): TodoState => {\n\t\tif (api.session) state = loadState(api);\n\t\treturn state;\n\t};\n\n\tconst persist = async (): Promise<TodoState> => {\n\t\tapi.session?.appendEntry(stateEntry, state);\n\t\tawait api.publish({ type: \"todo.changed\", version: 1, correlationId: stateEntry, data: state });\n\t\treturn state;\n\t};\n\n\tconst tool = api.registerTool({\n\t\tname: \"todo_update\",\n\t\tlabel: \"Update todo\",\n\t\tdescription: \"Update a todo that softly references a plan by id\",\n\t\tparameters: todoSchema,\n\t\trenderResult: renderTodoResult,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\trefreshState();\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst id = text(input.id, \"id\");\n\t\t\tconst planId = text(input.planId, \"planId\");\n\t\t\tconst title = text(input.title, \"title\");\n\t\t\tconst nextRevision = revision(input.revision);\n\t\t\tconst current = state.todos.find((item) => item.id === id);\n\t\t\tif (current && nextRevision <= current.revision) throw new Error(`stale todo revision: ${id}`);\n\t\t\tif (current?.status === \"done\") throw new Error(`todo is terminal and cannot be updated: ${id}`);\n\t\t\tconst status =\n\t\t\t\tinput.status === undefined\n\t\t\t\t\t? (current?.status ?? \"pending\")\n\t\t\t\t\t: (text(input.status, \"status\") as TodoRecord[\"status\"]);\n\t\t\tif (![\"pending\", \"done\", \"blocked\"].includes(status)) throw new Error(\"invalid todo status\");\n\t\t\tconst values = evidence(input.evidence);\n\t\t\tif (status === \"done\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo completion requires at least one evidence reference\");\n\t\t\t}\n\t\t\tconst record: TodoRecord = { id, planId, title, status, revision: nextRevision, evidence: values };\n\t\t\tstate = { ...state, todos: [...state.todos.filter((item) => item.id !== id), record] };\n\t\t\tconst saved = await persist();\n\t\t\tif (planKnownToSession(api, planId)) return saved;\n\t\t\treturn {\n\t\t\t\t...saved,\n\t\t\t\twarning: `planId \"${planId}\" does not match any plan in this session's plan state; the todo keeps the soft reference, but the plan is unknown`,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst command = api.registerCommand({\n\t\tname: \"todo\",\n\t\tdescription: \"Show this session's todos (status and plan reference)\",\n\t\texecute: async (args: string) => {\n\t\t\ttry {\n\t\t\t\tif (args.trim() !== \"\") return { status: \"error\" as const, message: \"Usage: /todo\" };\n\t\t\t\treturn { status: \"success\" as const, message: renderTodoState(refreshState()) };\n\t\t\t} catch (error) {\n\t\t\t\t// A slash command must not crash the host (memory /promote precedent).\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\treturn [tool, command];\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Todo-set capability plugin: a self-contained todo state machine, extracted\n * from the host's `src/capabilities/todo-set.ts` (D-075 S4, first B-class\n * batch).\n *\n * Todos persist as append-only `todo.state` session entries and every change\n * is announced as a `todo.changed` event. `planId` is a soft reference: a\n * plain string whose target is checked ADVISORILY against the plan plugin's\n * LATEST `plan.state` snapshot (the same latest-snapshot semantics as this\n * plugin's own {@link loadState}); an unknown id yields a response warning,\n * never a failure (the same #10 pattern as the plan plugin's goalId check).\n * The plugin only consumes the public `session`, `events`, and `tools`\n * capabilities and never writes state owned by another plugin.\n */\n\nimport type { CapabilityAPI, DisposableRegistration } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\n\nexport interface TodoRecord {\n\tid: string;\n\t/** Soft reference: a plain string; unknown ids yield a response warning, not a failure. */\n\tplanId: string;\n\ttitle: string;\n\t/**\n\t * Lifecycle status. `cancelled` is terminal (voided: permanently retired,\n\t * no longer part of execution) and shares done's no-further-update guard.\n\t */\n\tstatus: \"pending\" | \"done\" | \"blocked\" | \"cancelled\";\n\trevision: number;\n\tevidence: string[];\n}\n\nexport interface TodoState {\n\ttodos: TodoRecord[];\n}\n\nconst stateEntry = \"todo.state\";\n/** Session protocol key of the plan plugin's snapshot entry (plugins/plan). */\nconst planStateEntry = \"plan.state\";\n\nconst todoSchema = Type.Object({\n\tid: Type.String({ description: \"Stable id of the todo to create or update\" }),\n\tplanId: Type.String({\n\t\tdescription:\n\t\t\t\"Soft reference to a plan id; an id unknown to this session's plan state yields a response warning, not a failure\",\n\t}),\n\ttitle: Type.String({ description: \"Human-readable todo title\" }),\n\trevision: Type.Integer({\n\t\tdescription: \"New revision counter; must be greater than the stored revision of an existing todo\",\n\t\tminimum: 0,\n\t}),\n\tstatus: Type.Optional(\n\t\tType.Union([Type.Literal(\"pending\"), Type.Literal(\"done\"), Type.Literal(\"blocked\"), Type.Literal(\"cancelled\")], {\n\t\t\tdescription:\n\t\t\t\t\"Next status; defaults to the stored status for existing todos, otherwise pending. done and cancelled are terminal: once a todo holds either status it can never be updated again, so finish every evidence reference before marking it done, and never mark done to avoid reporting unfinished work. Use blocked only for temporarily stuck work that may resume; use cancelled when the user retires or skips the todo\",\n\t\t}),\n\t),\n\tevidence: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Final evidence references; at least one is required when status is done or cancelled (for cancelled, cite the user's words that retired the todo). The list is fixed at that moment — a terminal todo cannot be updated to add later evidence such as a commit id\",\n\t\t}),\n\t),\n});\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Todo input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction text(value: unknown, name: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${name} must be a non-empty string`);\n\treturn value;\n}\n\nfunction revision(value: unknown): number {\n\tif (!Number.isInteger(value) || (value as number) < 0) throw new Error(\"revision must be a non-negative integer\");\n\treturn value as number;\n}\n\nfunction evidence(value: unknown): string[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value) || value.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new Error(\"evidence must be a string array\");\n\t}\n\treturn [...(value as string[])];\n}\n\nfunction loadState(api: CapabilityAPI): TodoState {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tconst entry = [...entries].reverse().find((item) => item.customType === stateEntry);\n\tif (!entry?.data || typeof entry.data !== \"object\") return { todos: [] };\n\tconst data = entry.data as Partial<TodoState>;\n\treturn { todos: Array.isArray(data.todos) ? [...data.todos] : [] };\n}\n\n/**\n * `/todo` command text rendering: one line per todo (`id [status → planId]\n * title`) plus the evidence lines that justified completion. The leading count\n * line lists its non-zero segments in done → blocked → cancelled order. Plain\n * text only —\n * every host shape can render it, no TUI required (the same L1 contract as\n * the workflow plugin's `/workflow`). Collapsed tool blocks pass\n * `{ evidence: false }` to omit the evidence lines; the default keeps the\n * command output byte-for-byte stable.\n */\nexport function renderTodoState(state: TodoState, options?: { evidence?: boolean }): string {\n\tconst withEvidence = options?.evidence ?? true;\n\tif (state.todos.length === 0) return \"no todos in this session; create one with todo_update\";\n\tconst done = state.todos.filter((todo) => todo.status === \"done\").length;\n\tconst blocked = state.todos.filter((todo) => todo.status === \"blocked\").length;\n\tconst cancelled = state.todos.filter((todo) => todo.status === \"cancelled\").length;\n\t// Count segments in the fixed done → blocked → cancelled order; zero\n\t// segments are omitted (the rule blocked always had), so a total with no\n\t// countable status renders bare instead of with empty parentheses.\n\tconst segments: string[] = [];\n\tif (done > 0) segments.push(`${done} done`);\n\tif (blocked > 0) segments.push(`${blocked} blocked`);\n\tif (cancelled > 0) segments.push(`${cancelled} cancelled`);\n\tconst lines = [\n\t\tsegments.length > 0 ? `todos: ${state.todos.length} (${segments.join(\", \")})` : `todos: ${state.todos.length}`,\n\t];\n\t// Record fields only validate non-emptiness, so fold newlines to keep the\n\t// one-line-per-record shape whatever the model wrote.\n\tconst inline = (value: string): string => value.replace(/\\n/g, \" \");\n\tfor (const todo of state.todos) {\n\t\tlines.push(`- ${inline(todo.id)} [${todo.status} → ${inline(todo.planId)}] ${inline(todo.title)}`);\n\t\tif (withEvidence) for (const item of todo.evidence) lines.push(` evidence: ${inline(item)}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Structural slices of the host tool-render contracts the renderer needs.\n * Declared locally on purpose: this plugin must stay self-contained and must\n * not import `@agent-forge/tui` (the tool-definition.ts structural hosting\n * boundary), so renderResult talks to these shapes instead of the real\n * Theme/Component types.\n */\ninterface ToolResultTextPartLike {\n\treadonly type: string;\n\treadonly text?: string;\n}\n\ninterface ToolResultLike {\n\treadonly content: readonly ToolResultTextPartLike[];\n}\n\ninterface RenderResultOptionsLike {\n\treadonly expanded: boolean;\n\treadonly isPartial: boolean;\n}\n\ninterface ThemeLike {\n\tfg(color: string, text: string): string;\n}\n\ninterface RenderContextLike {\n\treadonly lastComponent?: unknown;\n}\n\n/**\n * Simplified East Asian width table: these ranges cover the CJK blocks state\n * titles realistically use and count as 2 columns each; every other code\n * point counts as 1. Not a full wcwidth — combining marks and ambiguous\n * widths fall back to 1, the safe (narrower) default for row folding.\n */\nfunction isWideCodePoint(code: number): boolean {\n\treturn (\n\t\t(code >= 0x1100 && code <= 0x115f) ||\n\t\t(code >= 0x2e80 && code <= 0xa4cf) ||\n\t\t(code >= 0xac00 && code <= 0xd7a3) ||\n\t\t(code >= 0xf900 && code <= 0xfaff) ||\n\t\t(code >= 0xfe30 && code <= 0xfe4f) ||\n\t\t(code >= 0xff00 && code <= 0xff60) ||\n\t\t(code >= 0xffe0 && code <= 0xffe6) ||\n\t\t(code >= 0x20000 && code <= 0x3fffd)\n\t);\n}\n\n/** Folds one logical line into rows of at most `width` display columns. */\nfunction wrapLine(line: string, width: number): string[] {\n\tif (width <= 0) return [line];\n\tconst rows: string[] = [];\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of line) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (row !== \"\" && columns + cost > width) {\n\t\t\trows.push(row);\n\t\t\trow = character;\n\t\t\tcolumns = cost;\n\t\t} else {\n\t\t\trow += character;\n\t\t\tcolumns += cost;\n\t\t}\n\t}\n\trows.push(row);\n\treturn rows;\n}\n\n/**\n * Structural TUI component rendering a todo_update result as the plain state\n * listing: `warning: ` lines take the host theme's warning color, every other\n * row toolOutput (color is decided per logical line, so a wrapped warning\n * stays warning-colored). Rows are cached per width — the host may render the\n * same result repeatedly at one width — and dropped on text updates or\n * invalidate.\n */\nclass TodoResultComponent {\n\tprivate text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly rowsByWidth = new Map<number, string[]>();\n\n\tconstructor(text: string, theme: ThemeLike) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t}\n\n\tsetText(text: string): void {\n\t\tif (text === this.text) return;\n\t\tthis.text = text;\n\t\tthis.rowsByWidth.clear();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst cached = this.rowsByWidth.get(width);\n\t\tif (cached) return cached;\n\t\tconst rows: string[] = [];\n\t\tfor (const line of this.text.split(\"\\n\")) {\n\t\t\tconst color = line.startsWith(\"warning: \") ? \"warning\" : \"toolOutput\";\n\t\t\tfor (const row of wrapLine(line, width)) rows.push(this.theme.fg(color, row));\n\t\t}\n\t\tthis.rowsByWidth.set(width, rows);\n\t\treturn rows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.rowsByWidth.clear();\n\t}\n}\n\n/**\n * Parses the text part the host serialized from this plugin's execute return\n * (capability-runtime-guards normalization): a `TodoState`, optionally with a\n * response-only `warning`. Anything else — a JSON.parse failure or a\n * different shape — returns undefined so the renderer falls back to the raw\n * text.\n */\nfunction parseTodoResultText(text: string): { state: TodoState; warning?: string } | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as { todos?: unknown; warning?: unknown };\n\tif (!Array.isArray(record.todos)) return undefined;\n\tif (record.warning !== undefined && typeof record.warning !== \"string\") return undefined;\n\tconst state: TodoState = { todos: record.todos as TodoRecord[] };\n\treturn record.warning === undefined ? { state } : { state, warning: record.warning };\n}\n\n/**\n * renderResult for todo_update: re-renders the tool's own state payload as\n * the readable listing instead of the host's generic one-line JSON fallback.\n * Collapsed blocks show the summary and record lines only; expanded blocks\n * add the evidence lines; a response `warning` rides along as a final\n * warning-colored line. Non-state payloads and malformed records render\n * verbatim as toolOutput.\n */\nfunction renderTodoResult(\n\tresult: ToolResultLike,\n\toptions: RenderResultOptionsLike,\n\ttheme: ThemeLike,\n\tcontext: RenderContextLike,\n): TodoResultComponent | undefined {\n\tconst text = result.content.find((part) => part.type === \"text\")?.text;\n\tif (text === undefined || text === \"\") return undefined;\n\tconst parsed = parseTodoResultText(text);\n\tlet listing: string;\n\tif (parsed) {\n\t\t// The parse gate is envelope-level, so a parsed state may still hold\n\t\t// malformed records (null elements, a record missing evidence).\n\t\t// Rendering those must not throw out of the plugin: fall back to the\n\t\t// raw text, the same path as a parse failure.\n\t\ttry {\n\t\t\tlisting = renderTodoState(parsed.state, { evidence: options.expanded });\n\t\t\tif (parsed.warning !== undefined) {\n\t\t\t\t// Fold newlines like the record fields so the warning stays one line.\n\t\t\t\tlisting = `${listing}\\nwarning: ${parsed.warning.replace(/\\n/g, \" \")}`;\n\t\t\t}\n\t\t} catch {\n\t\t\tlisting = text;\n\t\t}\n\t} else {\n\t\tlisting = text;\n\t}\n\t// Reuse the host-provided component for this render slot (the ls.ts\n\t// precedent): same instance in, same instance out, with fresh text.\n\tif (context.lastComponent instanceof TodoResultComponent) {\n\t\tcontext.lastComponent.setText(listing);\n\t\treturn context.lastComponent;\n\t}\n\treturn new TodoResultComponent(listing, theme);\n}\n\n/**\n * Whether the session branch's LATEST `plan.state` snapshot knows a plan with\n * the given id (the same latest-snapshot semantics as {@link loadState} and\n * the same advisory pattern as the plan plugin's goalKnownToSession). The\n * check is advisory only (#10): a missing plan downgrades to a warning in the\n * tool response — the soft-reference write path never fails on it.\n */\nfunction planKnownToSession(api: CapabilityAPI, planId: string): boolean {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.customType !== planStateEntry) continue;\n\t\tconst data = entry.data;\n\t\tif (!data || typeof data !== \"object\" || Array.isArray(data)) return false;\n\t\tconst plans = (data as { plans?: unknown }).plans;\n\t\tif (!Array.isArray(plans)) return false;\n\t\treturn plans.some(\n\t\t\t(plan) => plan !== null && typeof plan === \"object\" && (plan as { id?: unknown }).id === planId,\n\t\t);\n\t}\n\treturn false;\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n): Promise<readonly DisposableRegistration[]> | readonly DisposableRegistration[] {\n\tlet state = loadState(api);\n\tconst refreshState = (): TodoState => {\n\t\tif (api.session) state = loadState(api);\n\t\treturn state;\n\t};\n\n\tconst persist = async (): Promise<TodoState> => {\n\t\tapi.session?.appendEntry(stateEntry, state);\n\t\tawait api.publish({ type: \"todo.changed\", version: 1, correlationId: stateEntry, data: state });\n\t\treturn state;\n\t};\n\n\tconst tool = api.registerTool({\n\t\tname: \"todo_update\",\n\t\tlabel: \"Update todo\",\n\t\tdescription:\n\t\t\t\"Update a todo that softly references a plan by id. Create with status pending at revision 1, update unfinished items with a higher revision; marking done or cancelled is final and requires evidence, so commit the work first and then complete the todo\",\n\t\tparameters: todoSchema,\n\t\trenderResult: renderTodoResult,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\trefreshState();\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst id = text(input.id, \"id\");\n\t\t\tconst planId = text(input.planId, \"planId\");\n\t\t\tconst title = text(input.title, \"title\");\n\t\t\tconst nextRevision = revision(input.revision);\n\t\t\tconst current = state.todos.find((item) => item.id === id);\n\t\t\tif (current && nextRevision <= current.revision) throw new Error(`stale todo revision: ${id}`);\n\t\t\tif (current?.status === \"done\" || current?.status === \"cancelled\") {\n\t\t\t\tthrow new Error(`todo is terminal and cannot be updated: ${id}`);\n\t\t\t}\n\t\t\tconst status =\n\t\t\t\tinput.status === undefined\n\t\t\t\t\t? (current?.status ?? \"pending\")\n\t\t\t\t\t: (text(input.status, \"status\") as TodoRecord[\"status\"]);\n\t\t\tif (![\"pending\", \"done\", \"blocked\", \"cancelled\"].includes(status)) throw new Error(\"invalid todo status\");\n\t\t\tconst values = evidence(input.evidence);\n\t\t\tif (status === \"done\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo completion requires at least one evidence reference\");\n\t\t\t}\n\t\t\tif (status === \"cancelled\" && values.length === 0) {\n\t\t\t\tthrow new Error(\"todo cancellation requires at least one evidence reference\");\n\t\t\t}\n\t\t\tconst record: TodoRecord = { id, planId, title, status, revision: nextRevision, evidence: values };\n\t\t\tstate = { ...state, todos: [...state.todos.filter((item) => item.id !== id), record] };\n\t\t\tconst saved = await persist();\n\t\t\tif (planKnownToSession(api, planId)) return saved;\n\t\t\treturn {\n\t\t\t\t...saved,\n\t\t\t\twarning: `planId \"${planId}\" does not match any plan in this session's plan state; the todo keeps the soft reference, but the plan is unknown`,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst command = api.registerCommand({\n\t\tname: \"todo\",\n\t\tdescription: \"Show this session's todos (status and plan reference)\",\n\t\texecute: async (args: string) => {\n\t\t\ttry {\n\t\t\t\tif (args.trim() !== \"\") return { status: \"error\" as const, message: \"Usage: /todo\" };\n\t\t\t\treturn { status: \"success\" as const, message: renderTodoState(refreshState()) };\n\t\t\t} catch (error) {\n\t\t\t\t// A slash command must not crash the host (memory /promote precedent).\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\treturn [tool, command];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-forge/plugin-todo-set",
3
- "version": "0.88.14",
3
+ "version": "0.88.16",
4
4
  "description": "Todo-set capability plugin for Agent Forge (todo_update state machine over session entries)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,11 +45,10 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "typebox": "1.3.27",
48
- "@ai-agent-forge/plugin-sdk": "^0.88.14"
48
+ "@ai-agent-forge/plugin-sdk": "^0.88.16"
49
49
  },
50
50
  "devDependencies": {
51
51
  "shx": "0.3.4",
52
- "vitest": "4.1.11",
53
- "@ai-agent-forge/agent-forge": "^0.88.14"
52
+ "vitest": "4.1.11"
54
53
  }
55
54
  }
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "agent-forge.plugin.todo-set",
3
- "version": "0.88.14",
3
+ "version": "0.88.16",
4
4
  "apiVersion": "1-draft",
5
5
  "entry": "./dist/entry.js",
6
6
  "hostVersion": ">=0.84.4",