@astralyn/pi 0.82.3 → 0.82.5

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.
Files changed (158) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli/config-selector.d.ts.map +1 -1
  3. package/dist/cli/config-selector.js +3 -2
  4. package/dist/cli/config-selector.js.map +1 -1
  5. package/dist/cli/startup-ui.d.ts.map +1 -1
  6. package/dist/cli/startup-ui.js +3 -2
  7. package/dist/cli/startup-ui.js.map +1 -1
  8. package/dist/core/agent-session.d.ts +2 -0
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +33 -14
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/extensions/index.d.ts +2 -2
  13. package/dist/core/extensions/index.d.ts.map +1 -1
  14. package/dist/core/extensions/index.js +1 -1
  15. package/dist/core/extensions/index.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts +1 -1
  17. package/dist/core/extensions/loader.d.ts.map +1 -1
  18. package/dist/core/extensions/loader.js +2 -3
  19. package/dist/core/extensions/loader.js.map +1 -1
  20. package/dist/core/extensions/types.d.ts +18 -0
  21. package/dist/core/extensions/types.d.ts.map +1 -1
  22. package/dist/core/extensions/types.js +11 -0
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/extensions/index.d.ts.map +1 -1
  25. package/dist/extensions/index.js +2 -0
  26. package/dist/extensions/index.js.map +1 -1
  27. package/dist/extensions/plan/constants.d.ts +31 -0
  28. package/dist/extensions/plan/constants.d.ts.map +1 -0
  29. package/dist/extensions/plan/constants.js +57 -0
  30. package/dist/extensions/plan/constants.js.map +1 -0
  31. package/dist/extensions/plan/index.d.ts +21 -0
  32. package/dist/extensions/plan/index.d.ts.map +1 -0
  33. package/dist/extensions/plan/index.js +291 -0
  34. package/dist/extensions/plan/index.js.map +1 -0
  35. package/dist/extensions/plan/schema.d.ts +15 -0
  36. package/dist/extensions/plan/schema.d.ts.map +1 -0
  37. package/dist/extensions/plan/schema.js +13 -0
  38. package/dist/extensions/plan/schema.js.map +1 -0
  39. package/dist/extensions/plan/state.d.ts +29 -0
  40. package/dist/extensions/plan/state.d.ts.map +1 -0
  41. package/dist/extensions/plan/state.js +74 -0
  42. package/dist/extensions/plan/state.js.map +1 -0
  43. package/dist/extensions/plan/storage.d.ts +18 -0
  44. package/dist/extensions/plan/storage.d.ts.map +1 -0
  45. package/dist/extensions/plan/storage.js +58 -0
  46. package/dist/extensions/plan/storage.js.map +1 -0
  47. package/dist/extensions/question/dialog.d.ts +3 -1
  48. package/dist/extensions/question/dialog.d.ts.map +1 -1
  49. package/dist/extensions/question/dialog.js +106 -52
  50. package/dist/extensions/question/dialog.js.map +1 -1
  51. package/dist/extensions/question/index.d.ts.map +1 -1
  52. package/dist/extensions/question/index.js +2 -2
  53. package/dist/extensions/question/index.js.map +1 -1
  54. package/dist/extensions/question/render-cache.d.ts +9 -0
  55. package/dist/extensions/question/render-cache.d.ts.map +1 -1
  56. package/dist/extensions/question/render-cache.js +22 -0
  57. package/dist/extensions/question/render-cache.js.map +1 -1
  58. package/dist/extensions/question/results.d.ts.map +1 -1
  59. package/dist/extensions/question/results.js +6 -1
  60. package/dist/extensions/question/results.js.map +1 -1
  61. package/dist/extensions/question/schema.d.ts +4 -0
  62. package/dist/extensions/question/schema.d.ts.map +1 -1
  63. package/dist/extensions/question/schema.js +19 -28
  64. package/dist/extensions/question/schema.js.map +1 -1
  65. package/dist/extensions/question/types.d.ts +1 -1
  66. package/dist/extensions/question/types.d.ts.map +1 -1
  67. package/dist/extensions/question/types.js.map +1 -1
  68. package/dist/extensions/rewind/anchor.d.ts +29 -0
  69. package/dist/extensions/rewind/anchor.d.ts.map +1 -0
  70. package/dist/extensions/rewind/anchor.js +35 -0
  71. package/dist/extensions/rewind/anchor.js.map +1 -0
  72. package/dist/extensions/rewind/engine.d.ts +2 -6
  73. package/dist/extensions/rewind/engine.d.ts.map +1 -1
  74. package/dist/extensions/rewind/engine.js +41 -10
  75. package/dist/extensions/rewind/engine.js.map +1 -1
  76. package/dist/extensions/rewind/index.d.ts.map +1 -1
  77. package/dist/extensions/rewind/index.js +55 -21
  78. package/dist/extensions/rewind/index.js.map +1 -1
  79. package/dist/extensions/rewind/menu.d.ts.map +1 -1
  80. package/dist/extensions/rewind/menu.js +7 -2
  81. package/dist/extensions/rewind/menu.js.map +1 -1
  82. package/dist/extensions/rewind/restore.d.ts +26 -9
  83. package/dist/extensions/rewind/restore.d.ts.map +1 -1
  84. package/dist/extensions/rewind/restore.js +58 -16
  85. package/dist/extensions/rewind/restore.js.map +1 -1
  86. package/dist/extensions/subagent/activity.d.ts +1 -0
  87. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  88. package/dist/extensions/subagent/activity.js +20 -4
  89. package/dist/extensions/subagent/activity.js.map +1 -1
  90. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  91. package/dist/extensions/subagent/agents.js +13 -18
  92. package/dist/extensions/subagent/agents.js.map +1 -1
  93. package/dist/extensions/subagent/constants.d.ts +5 -4
  94. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  95. package/dist/extensions/subagent/constants.js +12 -5
  96. package/dist/extensions/subagent/constants.js.map +1 -1
  97. package/dist/extensions/subagent/index.d.ts.map +1 -1
  98. package/dist/extensions/subagent/index.js +12 -5
  99. package/dist/extensions/subagent/index.js.map +1 -1
  100. package/dist/extensions/subagent/render.d.ts.map +1 -1
  101. package/dist/extensions/subagent/render.js +166 -41
  102. package/dist/extensions/subagent/render.js.map +1 -1
  103. package/dist/extensions/subagent/resolve.d.ts +2 -5
  104. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  105. package/dist/extensions/subagent/resolve.js +8 -13
  106. package/dist/extensions/subagent/resolve.js.map +1 -1
  107. package/dist/extensions/subagent/runner.d.ts +3 -1
  108. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  109. package/dist/extensions/subagent/runner.js +115 -66
  110. package/dist/extensions/subagent/runner.js.map +1 -1
  111. package/dist/extensions/subagent/schema.d.ts +0 -6
  112. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  113. package/dist/extensions/subagent/schema.js +3 -4
  114. package/dist/extensions/subagent/schema.js.map +1 -1
  115. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  116. package/dist/extensions/subagent/sdk-runner.js +37 -16
  117. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  118. package/dist/extensions/subagent/types.d.ts +10 -2
  119. package/dist/extensions/subagent/types.d.ts.map +1 -1
  120. package/dist/extensions/subagent/types.js.map +1 -1
  121. package/dist/extensions/todo/constants.d.ts +1 -1
  122. package/dist/extensions/todo/constants.d.ts.map +1 -1
  123. package/dist/extensions/todo/constants.js +29 -4
  124. package/dist/extensions/todo/constants.js.map +1 -1
  125. package/dist/extensions/todo/index.d.ts +1 -1
  126. package/dist/extensions/todo/index.d.ts.map +1 -1
  127. package/dist/extensions/todo/index.js +22 -3
  128. package/dist/extensions/todo/index.js.map +1 -1
  129. package/dist/extensions/todo/schema.d.ts +6 -5
  130. package/dist/extensions/todo/schema.d.ts.map +1 -1
  131. package/dist/extensions/todo/schema.js +20 -9
  132. package/dist/extensions/todo/schema.js.map +1 -1
  133. package/dist/extensions/todo/state.d.ts +5 -1
  134. package/dist/extensions/todo/state.d.ts.map +1 -1
  135. package/dist/extensions/todo/state.js +184 -38
  136. package/dist/extensions/todo/state.js.map +1 -1
  137. package/dist/extensions/todo/view.d.ts.map +1 -1
  138. package/dist/extensions/todo/view.js +10 -6
  139. package/dist/extensions/todo/view.js.map +1 -1
  140. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  141. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  142. package/dist/modes/interactive/interactive-mode.js +36 -2
  143. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  144. package/dist/utils/coalescing-terminal.d.ts +116 -0
  145. package/dist/utils/coalescing-terminal.d.ts.map +1 -0
  146. package/dist/utils/coalescing-terminal.js +256 -0
  147. package/dist/utils/coalescing-terminal.js.map +1 -0
  148. package/docs/bundled/extensions/README.md +1 -0
  149. package/docs/bundled/extensions/plan.md +31 -0
  150. package/docs/bundled/extensions/question.md +9 -7
  151. package/docs/bundled/extensions/rewind.md +19 -8
  152. package/docs/bundled/extensions/subagent.md +16 -16
  153. package/docs/bundled/extensions/todo.md +5 -3
  154. package/docs/compaction.md +6 -2
  155. package/docs/docs.json +4 -0
  156. package/docs/extensions.md +4 -2
  157. package/npm-shrinkwrap.json +2 -2
  158. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,qBAAqB,4xBACmvB,CAAC;AAEtxB,eAAO,MAAM,mBAAmB,yEAAyE,CAAC;AAE1G,eAAO,MAAM,sBAAsB,UAIlC,CAAC","sourcesContent":["/**\n * todo/constants.ts — tool identity, command name, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the tool, matching\n * the question/deepwiki constants.ts pattern. Prompt copy is the model-facing\n * contract; keep it stable.\n */\n\nexport const TODO_TOOL_NAME = \"todo\";\nexport const TODO_TOOL_LABEL = \"Todo\";\nexport const TODOS_COMMAND_NAME = \"todos\";\n\n/** Max tasks shown in a single `list` tool result body (model-facing). */\nexport const LIST_DISPLAY_MAX_ITEMS = 50;\n\nexport const TODO_TOOL_DESCRIPTION =\n\t\"Manage the conversation's task list for multi-step coding work. Actions: create, update, list, get, delete, clear. Tasks are created pending and are short, outcome-oriented units (imperative subjects, reviewable scope) with pending/in_progress/completed status and optional blockedBy dependencies (validated, cycle-checked); deleting a task releases its dependents and leaves an immutable tombstone available to get/list. Keep exactly one task in_progress: mark it before starting work, and mark it completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen a task instead of duplicating it. Use for work with three or more meaningful steps or user-provided task lists; skip it for trivial single-step tasks and simple Q&A.\";\n\nexport const TODO_PROMPT_SNIPPET = \"Track multi-step coding work with a small outcome-oriented task list\";\n\nexport const TODO_PROMPT_GUIDELINES = [\n\t\"Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; mark the active task in_progress before working on it and completed immediately after verification.\",\n\t\"Keep exactly one `todo` item in_progress at a time (the tool demotes any other active item to pending); never mark completed while tests fail, verification has not run, or work is partial — reopen instead of duplicating.\",\n\t\"Keep the `todo` list short and outcome-oriented; use blockedBy only for real prerequisites, and delete obsolete items promptly so their dependents are released.\",\n];\n"]}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,qBAAqB,4nEAyByF,CAAC;AAE5H,eAAO,MAAM,mBAAmB,yEAAyE,CAAC;AAE1G,eAAO,MAAM,sBAAsB,UAIlC,CAAC","sourcesContent":["/**\n * todo/constants.ts — tool identity, command name, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the tool, matching\n * the question/deepwiki constants.ts pattern. Prompt copy is the model-facing\n * contract; keep it stable.\n */\n\nexport const TODO_TOOL_NAME = \"todo\";\nexport const TODO_TOOL_LABEL = \"Todo\";\nexport const TODOS_COMMAND_NAME = \"todos\";\n\n/** Max tasks shown in a single `list` tool result body (model-facing). */\nexport const LIST_DISPLAY_MAX_ITEMS = 50;\n\nexport const TODO_TOOL_DESCRIPTION = `Manage the conversation's task list for multi-step coding work.\n\n## When to Use\n- Work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift\n- Capture new user requirements as tasks right away; after completing a task, add follow-up tasks discovered during implementation\n\n## When NOT to Use\n- Trivial single-step tasks and simple Q&A — just do the work\n\n## Actions\n- create: add a pending task; subject (short, imperative, reviewable scope) and description (what done means: acceptance criteria or verification detail) are required\n- update: change status or details, and manage dependencies via addBlockedBy/removeBlockedBy (what this task waits on) and addBlocks/removeBlocks (what waits on this task); dependencies are validated and cycle-checked\n- list: one-line summaries with unresolved blockers, optionally filtered by status; get: one task in full; delete: tombstone an obsolete task and release its dependents; clear: drop the list without reusing ids\n- Parameters that do not apply to the chosen action are rejected with guidance\n\n## Status Workflow\npending -> in_progress -> completed; deleted is the only terminal state. Keep exactly one task in_progress (starting one demotes any other active task): mark it in_progress BEFORE starting the work, and completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen instead of duplicating. When blocked, do not fake completion: create a task for the blocker and link it with addBlockedBy.\n\n## Examples\n{\"action\": \"create\", \"subject\": \"Fix login redirect\", \"description\": \"Redirect lands on the dashboard; auth tests pass\"}\n{\"action\": \"update\", \"id\": 2, \"status\": \"in_progress\", \"activeForm\": \"fixing login redirect\"}\n{\"action\": \"update\", \"id\": 1, \"addBlocks\": [2, 3]}\n\n## Tips\n- list before create to avoid duplicates; prefer working in id order — earlier tasks usually set up context for later ones\n- Use dependencies only for real ordering constraints, and delete obsolete tasks promptly so their dependents are released`;\n\nexport const TODO_PROMPT_SNIPPET = \"Track multi-step coding work with a small outcome-oriented task list\";\n\nexport const TODO_PROMPT_GUIDELINES = [\n\t\"Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; list before creating to avoid duplicates, and capture follow-up work discovered during implementation as new tasks.\",\n\t\"Mark the active `todo` task in_progress before starting work (exactly one at a time; the tool demotes any other active item) and completed immediately after verification — never while tests fail or work is partial; reopen instead of duplicating, and when blocked create a task for the blocker and link it with addBlockedBy.\",\n\t\"Keep the `todo` list short and outcome-oriented; prefer working in id order, use dependencies only for real prerequisites, and delete obsolete items promptly so their dependents are released.\",\n];\n"]}
@@ -10,11 +10,36 @@ export const TODO_TOOL_LABEL = "Todo";
10
10
  export const TODOS_COMMAND_NAME = "todos";
11
11
  /** Max tasks shown in a single `list` tool result body (model-facing). */
12
12
  export const LIST_DISPLAY_MAX_ITEMS = 50;
13
- export const TODO_TOOL_DESCRIPTION = "Manage the conversation's task list for multi-step coding work. Actions: create, update, list, get, delete, clear. Tasks are created pending and are short, outcome-oriented units (imperative subjects, reviewable scope) with pending/in_progress/completed status and optional blockedBy dependencies (validated, cycle-checked); deleting a task releases its dependents and leaves an immutable tombstone available to get/list. Keep exactly one task in_progress: mark it before starting work, and mark it completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen a task instead of duplicating it. Use for work with three or more meaningful steps or user-provided task lists; skip it for trivial single-step tasks and simple Q&A.";
13
+ export const TODO_TOOL_DESCRIPTION = `Manage the conversation's task list for multi-step coding work.
14
+
15
+ ## When to Use
16
+ - Work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift
17
+ - Capture new user requirements as tasks right away; after completing a task, add follow-up tasks discovered during implementation
18
+
19
+ ## When NOT to Use
20
+ - Trivial single-step tasks and simple Q&A — just do the work
21
+
22
+ ## Actions
23
+ - create: add a pending task; subject (short, imperative, reviewable scope) and description (what done means: acceptance criteria or verification detail) are required
24
+ - update: change status or details, and manage dependencies via addBlockedBy/removeBlockedBy (what this task waits on) and addBlocks/removeBlocks (what waits on this task); dependencies are validated and cycle-checked
25
+ - list: one-line summaries with unresolved blockers, optionally filtered by status; get: one task in full; delete: tombstone an obsolete task and release its dependents; clear: drop the list without reusing ids
26
+ - Parameters that do not apply to the chosen action are rejected with guidance
27
+
28
+ ## Status Workflow
29
+ pending -> in_progress -> completed; deleted is the only terminal state. Keep exactly one task in_progress (starting one demotes any other active task): mark it in_progress BEFORE starting the work, and completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen instead of duplicating. When blocked, do not fake completion: create a task for the blocker and link it with addBlockedBy.
30
+
31
+ ## Examples
32
+ {"action": "create", "subject": "Fix login redirect", "description": "Redirect lands on the dashboard; auth tests pass"}
33
+ {"action": "update", "id": 2, "status": "in_progress", "activeForm": "fixing login redirect"}
34
+ {"action": "update", "id": 1, "addBlocks": [2, 3]}
35
+
36
+ ## Tips
37
+ - list before create to avoid duplicates; prefer working in id order — earlier tasks usually set up context for later ones
38
+ - Use dependencies only for real ordering constraints, and delete obsolete tasks promptly so their dependents are released`;
14
39
  export const TODO_PROMPT_SNIPPET = "Track multi-step coding work with a small outcome-oriented task list";
15
40
  export const TODO_PROMPT_GUIDELINES = [
16
- "Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; mark the active task in_progress before working on it and completed immediately after verification.",
17
- "Keep exactly one `todo` item in_progress at a time (the tool demotes any other active item to pending); never mark completed while tests fail, verification has not run, or work is partial — reopen instead of duplicating.",
18
- "Keep the `todo` list short and outcome-oriented; use blockedBy only for real prerequisites, and delete obsolete items promptly so their dependents are released.",
41
+ "Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; list before creating to avoid duplicates, and capture follow-up work discovered during implementation as new tasks.",
42
+ "Mark the active `todo` task in_progress before starting work (exactly one at a time; the tool demotes any other active item) and completed immediately after verification — never while tests fail or work is partial; reopen instead of duplicating, and when blocked create a task for the blocker and link it with addBlockedBy.",
43
+ "Keep the `todo` list short and outcome-oriented; prefer working in id order, use dependencies only for real prerequisites, and delete obsolete items promptly so their dependents are released.",
19
44
  ];
20
45
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/extensions/todo/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,0EAA0E;AAC1E,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,MAAM,CAAC,MAAM,qBAAqB,GACjC,sxBAAoxB,CAAC;AAEtxB,MAAM,CAAC,MAAM,mBAAmB,GAAG,sEAAsE,CAAC;AAE1G,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACrC,mOAAmO;IACnO,gOAA8N;IAC9N,kKAAkK;CAClK,CAAC","sourcesContent":["/**\n * todo/constants.ts — tool identity, command name, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the tool, matching\n * the question/deepwiki constants.ts pattern. Prompt copy is the model-facing\n * contract; keep it stable.\n */\n\nexport const TODO_TOOL_NAME = \"todo\";\nexport const TODO_TOOL_LABEL = \"Todo\";\nexport const TODOS_COMMAND_NAME = \"todos\";\n\n/** Max tasks shown in a single `list` tool result body (model-facing). */\nexport const LIST_DISPLAY_MAX_ITEMS = 50;\n\nexport const TODO_TOOL_DESCRIPTION =\n\t\"Manage the conversation's task list for multi-step coding work. Actions: create, update, list, get, delete, clear. Tasks are created pending and are short, outcome-oriented units (imperative subjects, reviewable scope) with pending/in_progress/completed status and optional blockedBy dependencies (validated, cycle-checked); deleting a task releases its dependents and leaves an immutable tombstone available to get/list. Keep exactly one task in_progress: mark it before starting work, and mark it completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen a task instead of duplicating it. Use for work with three or more meaningful steps or user-provided task lists; skip it for trivial single-step tasks and simple Q&A.\";\n\nexport const TODO_PROMPT_SNIPPET = \"Track multi-step coding work with a small outcome-oriented task list\";\n\nexport const TODO_PROMPT_GUIDELINES = [\n\t\"Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; mark the active task in_progress before working on it and completed immediately after verification.\",\n\t\"Keep exactly one `todo` item in_progress at a time (the tool demotes any other active item to pending); never mark completed while tests fail, verification has not run, or work is partial — reopen instead of duplicating.\",\n\t\"Keep the `todo` list short and outcome-oriented; use blockedBy only for real prerequisites, and delete obsolete items promptly so their dependents are released.\",\n];\n"]}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/extensions/todo/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,0EAA0E;AAC1E,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;2HAyBsF,CAAC;AAE5H,MAAM,CAAC,MAAM,mBAAmB,GAAG,sEAAsE,CAAC;AAE1G,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACrC,mPAAmP;IACnP,uUAAqU;IACrU,iMAAiM;CACjM,CAAC","sourcesContent":["/**\n * todo/constants.ts — tool identity, command name, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the tool, matching\n * the question/deepwiki constants.ts pattern. Prompt copy is the model-facing\n * contract; keep it stable.\n */\n\nexport const TODO_TOOL_NAME = \"todo\";\nexport const TODO_TOOL_LABEL = \"Todo\";\nexport const TODOS_COMMAND_NAME = \"todos\";\n\n/** Max tasks shown in a single `list` tool result body (model-facing). */\nexport const LIST_DISPLAY_MAX_ITEMS = 50;\n\nexport const TODO_TOOL_DESCRIPTION = `Manage the conversation's task list for multi-step coding work.\n\n## When to Use\n- Work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift\n- Capture new user requirements as tasks right away; after completing a task, add follow-up tasks discovered during implementation\n\n## When NOT to Use\n- Trivial single-step tasks and simple Q&A — just do the work\n\n## Actions\n- create: add a pending task; subject (short, imperative, reviewable scope) and description (what done means: acceptance criteria or verification detail) are required\n- update: change status or details, and manage dependencies via addBlockedBy/removeBlockedBy (what this task waits on) and addBlocks/removeBlocks (what waits on this task); dependencies are validated and cycle-checked\n- list: one-line summaries with unresolved blockers, optionally filtered by status; get: one task in full; delete: tombstone an obsolete task and release its dependents; clear: drop the list without reusing ids\n- Parameters that do not apply to the chosen action are rejected with guidance\n\n## Status Workflow\npending -> in_progress -> completed; deleted is the only terminal state. Keep exactly one task in_progress (starting one demotes any other active task): mark it in_progress BEFORE starting the work, and completed only when implementation and verification are genuinely done — never with failing tests or partial work; reopen instead of duplicating. When blocked, do not fake completion: create a task for the blocker and link it with addBlockedBy.\n\n## Examples\n{\"action\": \"create\", \"subject\": \"Fix login redirect\", \"description\": \"Redirect lands on the dashboard; auth tests pass\"}\n{\"action\": \"update\", \"id\": 2, \"status\": \"in_progress\", \"activeForm\": \"fixing login redirect\"}\n{\"action\": \"update\", \"id\": 1, \"addBlocks\": [2, 3]}\n\n## Tips\n- list before create to avoid duplicates; prefer working in id order — earlier tasks usually set up context for later ones\n- Use dependencies only for real ordering constraints, and delete obsolete tasks promptly so their dependents are released`;\n\nexport const TODO_PROMPT_SNIPPET = \"Track multi-step coding work with a small outcome-oriented task list\";\n\nexport const TODO_PROMPT_GUIDELINES = [\n\t\"Use `todo` for work with three or more meaningful steps, user-provided task lists, or long sessions where progress can drift; list before creating to avoid duplicates, and capture follow-up work discovered during implementation as new tasks.\",\n\t\"Mark the active `todo` task in_progress before starting work (exactly one at a time; the tool demotes any other active item) and completed immediately after verification — never while tests fail or work is partial; reopen instead of duplicating, and when blocked create a task for the blocker and link it with addBlockedBy.\",\n\t\"Keep the `todo` list short and outcome-oriented; prefer working in id order, use dependencies only for real prerequisites, and delete obsolete items promptly so their dependents are released.\",\n];\n"]}
@@ -12,6 +12,6 @@
12
12
  * change the session within one process, and execute + lifecycle handlers
13
13
  * re-point the active bucket before touching state.
14
14
  */
15
- import type { ExtensionAPI } from "../../core/extensions/types.ts";
15
+ import { type ExtensionAPI } from "../../core/extensions/types.ts";
16
16
  export default function todo(pi: ExtensionAPI): void;
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAmB,YAAY,EAAqB,MAAM,gCAAgC,CAAC;AAqCvG,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAyEnD","sourcesContent":["/**\n * todo — Pi-native task tracking for multi-step work.\n *\n * The state is intentionally conversation-backed: every tool result carries a\n * full snapshot in `details`, and lifecycle handlers replay the current branch.\n * This keeps /reload, compaction, and session-tree navigation aligned with the\n * conversation without adding a separate disk database. (Compaction-safe by\n * design: sessionManager.getBranch() returns the FULL branch history including\n * pre-compaction toolResult entries — only buildSessionContext summarizes.)\n *\n * State is keyed per session id (see state.ts): resume and /tree switches can\n * change the session within one process, and execute + lifecycle handlers\n * re-point the active bucket before touching state.\n */\nimport type { AgentToolResult, ExtensionAPI, ToolExecutionMode } from \"../../core/extensions/types.ts\";\nimport {\n\tTODO_PROMPT_GUIDELINES,\n\tTODO_PROMPT_SNIPPET,\n\tTODO_TOOL_DESCRIPTION,\n\tTODO_TOOL_LABEL,\n\tTODO_TOOL_NAME,\n\tTODOS_COMMAND_NAME,\n} from \"./constants.ts\";\nimport { TodoOverlay } from \"./overlay.ts\";\nimport { type TodoDetails, TodoParamsSchema } from \"./schema.ts\";\nimport {\n\tapplyTodoMutation,\n\tbuildTodoDetails,\n\tcommitTodoState,\n\tdisposeTodoSession,\n\tformatTodoContent,\n\tgetTodoState,\n\treplaceTodoState,\n\treplayTodosFromBranch,\n\tsetActiveTodoSession,\n} from \"./state.ts\";\nimport { formatCommandList } from \"./view.ts\";\n\ninterface TodoSessionCtx {\n\tsessionManager: { getBranch(): Iterable<unknown>; getSessionId(): string };\n}\n\nfunction safeReplay(ctx: TodoSessionCtx): void {\n\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\ttry {\n\t\treplaceTodoState(replayTodosFromBranch(ctx));\n\t} catch (error) {\n\t\tif (!/stale after session replacement/.test(String(error))) throw error;\n\t}\n}\n\nexport default function todo(pi: ExtensionAPI): void {\n\tlet overlay: TodoOverlay | undefined;\n\n\tpi.registerTool({\n\t\tname: TODO_TOOL_NAME,\n\t\tlabel: TODO_TOOL_LABEL,\n\t\tdescription: TODO_TOOL_DESCRIPTION,\n\t\tpromptSnippet: TODO_PROMPT_SNIPPET,\n\t\tpromptGuidelines: TODO_PROMPT_GUIDELINES,\n\t\tparameters: TodoParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<TodoDetails>> {\n\t\t\t// Re-point the active bucket: resume//tree can switch sessions between\n\t\t\t// lifecycle events and this call.\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tconst result = applyTodoMutation(getTodoState(), params);\n\t\t\tif (result.operation.kind === \"error\") {\n\t\t\t\tthrow new Error(result.operation.message);\n\t\t\t}\n\t\t\tcommitTodoState(result.state);\n\t\t\tconst text = formatTodoContent(result.operation, result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: buildTodoDetails(params, result.state, result.operation),\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(TODOS_COMMAND_NAME, {\n\t\tdescription: \"Show todos for the current conversation branch\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"/todos requires an interactive UI.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tctx.ui.notify(formatCommandList(getTodoState()), \"info\");\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\tif (ctx.hasUI) {\n\t\t\toverlay ??= new TodoOverlay();\n\t\t\toverlay.setUI(ctx.ui);\n\t\t\toverlay.resetVisibility();\n\t\t\toverlay.update();\n\t\t}\n\t});\n\n\tpi.on(\"session_compact\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tdisposeTodoSession(ctx.sessionManager.getSessionId());\n\t\toverlay?.dispose();\n\t\toverlay = undefined;\n\t});\n\n\tpi.on(\"tool_execution_end\", async (event) => {\n\t\tif (event.toolName !== TODO_TOOL_NAME || event.isError) return;\n\t\toverlay?.update();\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEN,KAAK,YAAY,EAGjB,MAAM,gCAAgC,CAAC;AAyCxC,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAyEnD","sourcesContent":["/**\n * todo — Pi-native task tracking for multi-step work.\n *\n * The state is intentionally conversation-backed: every tool result carries a\n * full snapshot in `details`, and lifecycle handlers replay the current branch.\n * This keeps /reload, compaction, and session-tree navigation aligned with the\n * conversation without adding a separate disk database. (Compaction-safe by\n * design: sessionManager.getBranch() returns the FULL branch history including\n * pre-compaction toolResult entries — only buildSessionContext summarizes.)\n *\n * State is keyed per session id (see state.ts): resume and /tree switches can\n * change the session within one process, and execute + lifecycle handlers\n * re-point the active bucket before touching state.\n */\nimport {\n\ttype AgentToolResult,\n\ttype ExtensionAPI,\n\tisStaleExtensionContextError,\n\ttype ToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tTODO_PROMPT_GUIDELINES,\n\tTODO_PROMPT_SNIPPET,\n\tTODO_TOOL_DESCRIPTION,\n\tTODO_TOOL_LABEL,\n\tTODO_TOOL_NAME,\n\tTODOS_COMMAND_NAME,\n} from \"./constants.ts\";\nimport { TodoOverlay } from \"./overlay.ts\";\nimport { type TodoDetails, TodoParamsSchema } from \"./schema.ts\";\nimport {\n\tapplyTodoMutation,\n\tbuildTodoDetails,\n\tcommitTodoState,\n\tdisposeTodoSession,\n\tformatTodoContent,\n\tgetTodoState,\n\treplaceTodoState,\n\treplayTodosFromBranch,\n\tsetActiveTodoSession,\n} from \"./state.ts\";\nimport { formatCommandList } from \"./view.ts\";\n\ninterface TodoSessionCtx {\n\tsessionManager: { getBranch(): Iterable<unknown>; getSessionId(): string };\n}\n\nfunction safeReplay(ctx: TodoSessionCtx): void {\n\t// Every ctx.sessionManager access goes through a stale guard, so the whole\n\t// body stays inside the try: a lifecycle event can race session replacement\n\t// (resume, /tree, /reload), and a stale ctx just means another session took\n\t// over — nothing left to replay for it.\n\ttry {\n\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\treplaceTodoState(replayTodosFromBranch(ctx));\n\t} catch (error) {\n\t\tif (!isStaleExtensionContextError(error)) throw error;\n\t}\n}\n\nexport default function todo(pi: ExtensionAPI): void {\n\tlet overlay: TodoOverlay | undefined;\n\n\tpi.registerTool({\n\t\tname: TODO_TOOL_NAME,\n\t\tlabel: TODO_TOOL_LABEL,\n\t\tdescription: TODO_TOOL_DESCRIPTION,\n\t\tpromptSnippet: TODO_PROMPT_SNIPPET,\n\t\tpromptGuidelines: TODO_PROMPT_GUIDELINES,\n\t\tparameters: TodoParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<TodoDetails>> {\n\t\t\t// Re-point the active bucket: resume//tree can switch sessions between\n\t\t\t// lifecycle events and this call.\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tconst result = applyTodoMutation(getTodoState(), params);\n\t\t\tif (result.operation.kind === \"error\") {\n\t\t\t\tthrow new Error(result.operation.message);\n\t\t\t}\n\t\t\tcommitTodoState(result.state);\n\t\t\tconst text = formatTodoContent(result.operation, result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: buildTodoDetails(params, result.state),\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(TODOS_COMMAND_NAME, {\n\t\tdescription: \"Show todos for the current conversation branch\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"/todos requires an interactive UI.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tctx.ui.notify(formatCommandList(getTodoState()), \"info\");\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\tif (ctx.hasUI) {\n\t\t\toverlay ??= new TodoOverlay();\n\t\t\toverlay.setUI(ctx.ui);\n\t\t\toverlay.resetVisibility();\n\t\t\toverlay.update();\n\t\t}\n\t});\n\n\tpi.on(\"session_compact\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tdisposeTodoSession(ctx.sessionManager.getSessionId());\n\t\toverlay?.dispose();\n\t\toverlay = undefined;\n\t});\n\n\tpi.on(\"tool_execution_end\", async (event) => {\n\t\tif (event.toolName !== TODO_TOOL_NAME || event.isError) return;\n\t\toverlay?.update();\n\t});\n}\n"]}
@@ -1,15 +1,34 @@
1
+ /**
2
+ * todo — Pi-native task tracking for multi-step work.
3
+ *
4
+ * The state is intentionally conversation-backed: every tool result carries a
5
+ * full snapshot in `details`, and lifecycle handlers replay the current branch.
6
+ * This keeps /reload, compaction, and session-tree navigation aligned with the
7
+ * conversation without adding a separate disk database. (Compaction-safe by
8
+ * design: sessionManager.getBranch() returns the FULL branch history including
9
+ * pre-compaction toolResult entries — only buildSessionContext summarizes.)
10
+ *
11
+ * State is keyed per session id (see state.ts): resume and /tree switches can
12
+ * change the session within one process, and execute + lifecycle handlers
13
+ * re-point the active bucket before touching state.
14
+ */
15
+ import { isStaleExtensionContextError, } from "../../core/extensions/types.js";
1
16
  import { TODO_PROMPT_GUIDELINES, TODO_PROMPT_SNIPPET, TODO_TOOL_DESCRIPTION, TODO_TOOL_LABEL, TODO_TOOL_NAME, TODOS_COMMAND_NAME, } from "./constants.js";
2
17
  import { TodoOverlay } from "./overlay.js";
3
18
  import { TodoParamsSchema } from "./schema.js";
4
19
  import { applyTodoMutation, buildTodoDetails, commitTodoState, disposeTodoSession, formatTodoContent, getTodoState, replaceTodoState, replayTodosFromBranch, setActiveTodoSession, } from "./state.js";
5
20
  import { formatCommandList } from "./view.js";
6
21
  function safeReplay(ctx) {
7
- setActiveTodoSession(ctx.sessionManager.getSessionId());
22
+ // Every ctx.sessionManager access goes through a stale guard, so the whole
23
+ // body stays inside the try: a lifecycle event can race session replacement
24
+ // (resume, /tree, /reload), and a stale ctx just means another session took
25
+ // over — nothing left to replay for it.
8
26
  try {
27
+ setActiveTodoSession(ctx.sessionManager.getSessionId());
9
28
  replaceTodoState(replayTodosFromBranch(ctx));
10
29
  }
11
30
  catch (error) {
12
- if (!/stale after session replacement/.test(String(error)))
31
+ if (!isStaleExtensionContextError(error))
13
32
  throw error;
14
33
  }
15
34
  }
@@ -35,7 +54,7 @@ export default function todo(pi) {
35
54
  const text = formatTodoContent(result.operation, result.state);
36
55
  return {
37
56
  content: [{ type: "text", text }],
38
- details: buildTodoDetails(params, result.state, result.operation),
57
+ details: buildTodoDetails(params, result.state),
39
58
  };
40
59
  },
41
60
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/todo/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * todo — Pi-native task tracking for multi-step work.\n *\n * The state is intentionally conversation-backed: every tool result carries a\n * full snapshot in `details`, and lifecycle handlers replay the current branch.\n * This keeps /reload, compaction, and session-tree navigation aligned with the\n * conversation without adding a separate disk database. (Compaction-safe by\n * design: sessionManager.getBranch() returns the FULL branch history including\n * pre-compaction toolResult entries — only buildSessionContext summarizes.)\n *\n * State is keyed per session id (see state.ts): resume and /tree switches can\n * change the session within one process, and execute + lifecycle handlers\n * re-point the active bucket before touching state.\n */\nimport type { AgentToolResult, ExtensionAPI, ToolExecutionMode } from \"../../core/extensions/types.ts\";\nimport {\n\tTODO_PROMPT_GUIDELINES,\n\tTODO_PROMPT_SNIPPET,\n\tTODO_TOOL_DESCRIPTION,\n\tTODO_TOOL_LABEL,\n\tTODO_TOOL_NAME,\n\tTODOS_COMMAND_NAME,\n} from \"./constants.ts\";\nimport { TodoOverlay } from \"./overlay.ts\";\nimport { type TodoDetails, TodoParamsSchema } from \"./schema.ts\";\nimport {\n\tapplyTodoMutation,\n\tbuildTodoDetails,\n\tcommitTodoState,\n\tdisposeTodoSession,\n\tformatTodoContent,\n\tgetTodoState,\n\treplaceTodoState,\n\treplayTodosFromBranch,\n\tsetActiveTodoSession,\n} from \"./state.ts\";\nimport { formatCommandList } from \"./view.ts\";\n\ninterface TodoSessionCtx {\n\tsessionManager: { getBranch(): Iterable<unknown>; getSessionId(): string };\n}\n\nfunction safeReplay(ctx: TodoSessionCtx): void {\n\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\ttry {\n\t\treplaceTodoState(replayTodosFromBranch(ctx));\n\t} catch (error) {\n\t\tif (!/stale after session replacement/.test(String(error))) throw error;\n\t}\n}\n\nexport default function todo(pi: ExtensionAPI): void {\n\tlet overlay: TodoOverlay | undefined;\n\n\tpi.registerTool({\n\t\tname: TODO_TOOL_NAME,\n\t\tlabel: TODO_TOOL_LABEL,\n\t\tdescription: TODO_TOOL_DESCRIPTION,\n\t\tpromptSnippet: TODO_PROMPT_SNIPPET,\n\t\tpromptGuidelines: TODO_PROMPT_GUIDELINES,\n\t\tparameters: TodoParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<TodoDetails>> {\n\t\t\t// Re-point the active bucket: resume//tree can switch sessions between\n\t\t\t// lifecycle events and this call.\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tconst result = applyTodoMutation(getTodoState(), params);\n\t\t\tif (result.operation.kind === \"error\") {\n\t\t\t\tthrow new Error(result.operation.message);\n\t\t\t}\n\t\t\tcommitTodoState(result.state);\n\t\t\tconst text = formatTodoContent(result.operation, result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: buildTodoDetails(params, result.state, result.operation),\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(TODOS_COMMAND_NAME, {\n\t\tdescription: \"Show todos for the current conversation branch\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"/todos requires an interactive UI.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tctx.ui.notify(formatCommandList(getTodoState()), \"info\");\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\tif (ctx.hasUI) {\n\t\t\toverlay ??= new TodoOverlay();\n\t\t\toverlay.setUI(ctx.ui);\n\t\t\toverlay.resetVisibility();\n\t\t\toverlay.update();\n\t\t}\n\t});\n\n\tpi.on(\"session_compact\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tdisposeTodoSession(ctx.sessionManager.getSessionId());\n\t\toverlay?.dispose();\n\t\toverlay = undefined;\n\t});\n\n\tpi.on(\"tool_execution_end\", async (event) => {\n\t\tif (event.toolName !== TODO_TOOL_NAME || event.isError) return;\n\t\toverlay?.update();\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/todo/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * todo — Pi-native task tracking for multi-step work.\n *\n * The state is intentionally conversation-backed: every tool result carries a\n * full snapshot in `details`, and lifecycle handlers replay the current branch.\n * This keeps /reload, compaction, and session-tree navigation aligned with the\n * conversation without adding a separate disk database. (Compaction-safe by\n * design: sessionManager.getBranch() returns the FULL branch history including\n * pre-compaction toolResult entries — only buildSessionContext summarizes.)\n *\n * State is keyed per session id (see state.ts): resume and /tree switches can\n * change the session within one process, and execute + lifecycle handlers\n * re-point the active bucket before touching state.\n */\nimport {\n\ttype AgentToolResult,\n\ttype ExtensionAPI,\n\tisStaleExtensionContextError,\n\ttype ToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tTODO_PROMPT_GUIDELINES,\n\tTODO_PROMPT_SNIPPET,\n\tTODO_TOOL_DESCRIPTION,\n\tTODO_TOOL_LABEL,\n\tTODO_TOOL_NAME,\n\tTODOS_COMMAND_NAME,\n} from \"./constants.ts\";\nimport { TodoOverlay } from \"./overlay.ts\";\nimport { type TodoDetails, TodoParamsSchema } from \"./schema.ts\";\nimport {\n\tapplyTodoMutation,\n\tbuildTodoDetails,\n\tcommitTodoState,\n\tdisposeTodoSession,\n\tformatTodoContent,\n\tgetTodoState,\n\treplaceTodoState,\n\treplayTodosFromBranch,\n\tsetActiveTodoSession,\n} from \"./state.ts\";\nimport { formatCommandList } from \"./view.ts\";\n\ninterface TodoSessionCtx {\n\tsessionManager: { getBranch(): Iterable<unknown>; getSessionId(): string };\n}\n\nfunction safeReplay(ctx: TodoSessionCtx): void {\n\t// Every ctx.sessionManager access goes through a stale guard, so the whole\n\t// body stays inside the try: a lifecycle event can race session replacement\n\t// (resume, /tree, /reload), and a stale ctx just means another session took\n\t// over — nothing left to replay for it.\n\ttry {\n\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\treplaceTodoState(replayTodosFromBranch(ctx));\n\t} catch (error) {\n\t\tif (!isStaleExtensionContextError(error)) throw error;\n\t}\n}\n\nexport default function todo(pi: ExtensionAPI): void {\n\tlet overlay: TodoOverlay | undefined;\n\n\tpi.registerTool({\n\t\tname: TODO_TOOL_NAME,\n\t\tlabel: TODO_TOOL_LABEL,\n\t\tdescription: TODO_TOOL_DESCRIPTION,\n\t\tpromptSnippet: TODO_PROMPT_SNIPPET,\n\t\tpromptGuidelines: TODO_PROMPT_GUIDELINES,\n\t\tparameters: TodoParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<TodoDetails>> {\n\t\t\t// Re-point the active bucket: resume//tree can switch sessions between\n\t\t\t// lifecycle events and this call.\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tconst result = applyTodoMutation(getTodoState(), params);\n\t\t\tif (result.operation.kind === \"error\") {\n\t\t\t\tthrow new Error(result.operation.message);\n\t\t\t}\n\t\t\tcommitTodoState(result.state);\n\t\t\tconst text = formatTodoContent(result.operation, result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: buildTodoDetails(params, result.state),\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(TODOS_COMMAND_NAME, {\n\t\tdescription: \"Show todos for the current conversation branch\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"/todos requires an interactive UI.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetActiveTodoSession(ctx.sessionManager.getSessionId());\n\t\t\tctx.ui.notify(formatCommandList(getTodoState()), \"info\");\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\tif (ctx.hasUI) {\n\t\t\toverlay ??= new TodoOverlay();\n\t\t\toverlay.setUI(ctx.ui);\n\t\t\toverlay.resetVisibility();\n\t\t\toverlay.update();\n\t\t}\n\t});\n\n\tpi.on(\"session_compact\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsafeReplay(ctx);\n\t\toverlay?.resetVisibility();\n\t\toverlay?.update();\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tdisposeTodoSession(ctx.sessionManager.getSessionId());\n\t\toverlay?.dispose();\n\t\toverlay = undefined;\n\t});\n\n\tpi.on(\"tool_execution_end\", async (event) => {\n\t\tif (event.toolName !== TODO_TOOL_NAME || event.isError) return;\n\t\toverlay?.update();\n\t});\n}\n"]}
@@ -20,7 +20,6 @@ export interface TodoDetails {
20
20
  params: Record<string, unknown>;
21
21
  items: TodoItem[];
22
22
  nextId: number;
23
- error?: string;
24
23
  }
25
24
  export declare const TodoParamsSchema: Type.TObject<{
26
25
  action: Type.TUnsafe<"clear" | "create" | "delete" | "get" | "list" | "update">;
@@ -28,12 +27,14 @@ export declare const TodoParamsSchema: Type.TObject<{
28
27
  description: Type.TOptional<Type.TString>;
29
28
  activeForm: Type.TOptional<Type.TString>;
30
29
  status: Type.TOptional<Type.TUnsafe<"completed" | "deleted" | "in_progress" | "pending">>;
31
- blockedBy: Type.TOptional<Type.TArray<Type.TNumber>>;
32
- addBlockedBy: Type.TOptional<Type.TArray<Type.TNumber>>;
33
- removeBlockedBy: Type.TOptional<Type.TArray<Type.TNumber>>;
30
+ blockedBy: Type.TOptional<Type.TArray<Type.TInteger>>;
31
+ addBlockedBy: Type.TOptional<Type.TArray<Type.TInteger>>;
32
+ removeBlockedBy: Type.TOptional<Type.TArray<Type.TInteger>>;
33
+ addBlocks: Type.TOptional<Type.TArray<Type.TInteger>>;
34
+ removeBlocks: Type.TOptional<Type.TArray<Type.TInteger>>;
34
35
  owner: Type.TOptional<Type.TString>;
35
36
  metadata: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
36
- id: Type.TOptional<Type.TNumber>;
37
+ id: Type.TOptional<Type.TInteger>;
37
38
  includeDeleted: Type.TOptional<Type.TBoolean>;
38
39
  }>;
39
40
  export type TodoParams = Static<typeof TodoParamsSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAC7E,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnF,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAYD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;EAyC3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEzD,eAAO,MAAM,gBAAgB,EAAE,SAAoC,CAAC","sourcesContent":["import { StringEnum } from \"@earendil-works/pi-ai\";\nimport { type Static, Type } from \"typebox\";\n\nexport type TodoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"deleted\";\nexport type TodoAction = \"create\" | \"update\" | \"list\" | \"get\" | \"delete\" | \"clear\";\n\nexport interface TodoItem {\n\tid: number;\n\tsubject: string;\n\tdescription?: string;\n\tactiveForm?: string;\n\tstatus: TodoStatus;\n\tblockedBy?: number[];\n\towner?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface TodoState {\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nexport interface TodoDetails {\n\taction: TodoAction;\n\tparams: Record<string, unknown>;\n\titems: TodoItem[];\n\tnextId: number;\n\terror?: string;\n}\n\nconst StatusSchema = StringEnum([\"pending\", \"in_progress\", \"completed\", \"deleted\"] as const, {\n\tdescription:\n\t\t\"Task status: pending for future work, in_progress for the single active task, completed for verified done work, and deleted for an immutable obsolete-task tombstone. Set deleted through update or use the delete action. With action list, status also acts as a filter: only tasks with this status are listed.\",\n});\n\nconst ActionSchema = StringEnum([\"create\", \"update\", \"list\", \"get\", \"delete\", \"clear\"] as const, {\n\tdescription:\n\t\t\"Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids.\",\n});\n\nexport const TodoParamsSchema = Type.Object({\n\taction: ActionSchema,\n\tsubject: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Short imperative task subject, required for create; use a reviewable unit of work, not a micro-step.\",\n\t\t}),\n\t),\n\tdescription: Type.Optional(\n\t\tType.String({ description: \"Longer task notes, acceptance criteria, or verification detail.\" }),\n\t),\n\tactiveForm: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'.\",\n\t\t}),\n\t),\n\tstatus: Type.Optional(StatusSchema),\n\tblockedBy: Type.Optional(\n\t\tType.Array(Type.Number(), {\n\t\t\tdescription: \"Initial dependency ids for create; use only for real ordering constraints.\",\n\t\t}),\n\t),\n\taddBlockedBy: Type.Optional(\n\t\tType.Array(Type.Number(), {\n\t\t\tdescription: \"Dependency ids to add on update; dependencies must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlockedBy: Type.Optional(Type.Array(Type.Number(), { description: \"Dependency ids to remove on update.\" })),\n\towner: Type.Optional(Type.String({ description: \"Optional owner or agent label for multi-agent coordination.\" })),\n\tmetadata: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Optional structured metadata for integrations; null values delete keys on update.\",\n\t\t}),\n\t),\n\tid: Type.Optional(Type.Number({ description: \"Task id, required for update, get, and delete.\" })),\n\tincludeDeleted: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Include deleted tombstones in unfiltered list output; an explicit status=deleted query includes them automatically.\",\n\t\t}),\n\t),\n});\n\nexport type TodoParams = Static<typeof TodoParamsSchema>;\n\nexport const EMPTY_TODO_STATE: TodoState = { items: [], nextId: 1 };\n"]}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAC7E,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnF,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CACf;AAcD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;EAgE3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEzD,eAAO,MAAM,gBAAgB,EAAE,SAAoC,CAAC","sourcesContent":["import { StringEnum } from \"@earendil-works/pi-ai\";\nimport { type Static, Type } from \"typebox\";\n\nexport type TodoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"deleted\";\nexport type TodoAction = \"create\" | \"update\" | \"list\" | \"get\" | \"delete\" | \"clear\";\n\nexport interface TodoItem {\n\tid: number;\n\tsubject: string;\n\tdescription?: string;\n\tactiveForm?: string;\n\tstatus: TodoStatus;\n\tblockedBy?: number[];\n\towner?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface TodoState {\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nexport interface TodoDetails {\n\taction: TodoAction;\n\tparams: Record<string, unknown>;\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nconst StatusSchema = StringEnum([\"pending\", \"in_progress\", \"completed\", \"deleted\"] as const, {\n\tdescription:\n\t\t\"Task status: pending for future work, in_progress for the single active task, completed for verified done work, and deleted for an immutable obsolete-task tombstone. Set deleted through update or use the delete action. With action list, status also acts as a filter: only tasks with this status are listed.\",\n});\n\nconst ActionSchema = StringEnum([\"create\", \"update\", \"list\", \"get\", \"delete\", \"clear\"] as const, {\n\tdescription:\n\t\t\"Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids. Parameters that do not apply to the chosen action are rejected.\",\n});\n\nconst TaskId = (description: string) => Type.Integer({ minimum: 1, description });\n\nexport const TodoParamsSchema = Type.Object({\n\taction: ActionSchema,\n\tsubject: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Short imperative task subject, required for create; use a reviewable unit of work, not a micro-step.\",\n\t\t}),\n\t),\n\tdescription: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"What done means for this task: notes, acceptance criteria, or verification detail. Required for create; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tactiveForm: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tstatus: Type.Optional(StatusSchema),\n\tblockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id this task depends on.\"), {\n\t\t\tdescription:\n\t\t\t\t\"Initial dependency ids, create only (update rejects it; use addBlockedBy/removeBlockedBy there); use only for real ordering constraints.\",\n\t\t}),\n\t),\n\taddBlockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id that must complete first.\"), {\n\t\t\tdescription: \"Dependency ids to add on update; dependencies must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Dependency id to detach.\"), { description: \"Dependency ids to remove on update.\" }),\n\t),\n\taddBlocks: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id that must wait for this one.\"), {\n\t\t\tdescription:\n\t\t\t\t\"Update only: ids of tasks that cannot start until this task completes (the reverse of addBlockedBy); targets must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlocks: Type.Optional(\n\t\tType.Array(TaskId(\"Dependent task id to release.\"), {\n\t\t\tdescription: \"Update only: ids of tasks that should no longer wait for this task.\",\n\t\t}),\n\t),\n\towner: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional owner or agent label for multi-agent coordination; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tmetadata: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Optional structured metadata for integrations; null values delete keys on update.\",\n\t\t}),\n\t),\n\tid: Type.Optional(TaskId(\"Task id, required for update, get, and delete.\")),\n\tincludeDeleted: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Include deleted tombstones in unfiltered list output; an explicit status=deleted query includes them automatically.\",\n\t\t}),\n\t),\n});\n\nexport type TodoParams = Static<typeof TodoParamsSchema>;\n\nexport const EMPTY_TODO_STATE: TodoState = { items: [], nextId: 1 };\n"]}
@@ -4,30 +4,41 @@ const StatusSchema = StringEnum(["pending", "in_progress", "completed", "deleted
4
4
  description: "Task status: pending for future work, in_progress for the single active task, completed for verified done work, and deleted for an immutable obsolete-task tombstone. Set deleted through update or use the delete action. With action list, status also acts as a filter: only tasks with this status are listed.",
5
5
  });
6
6
  const ActionSchema = StringEnum(["create", "update", "list", "get", "delete", "clear"], {
7
- description: "Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids.",
7
+ description: "Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids. Parameters that do not apply to the chosen action are rejected.",
8
8
  });
9
+ const TaskId = (description) => Type.Integer({ minimum: 1, description });
9
10
  export const TodoParamsSchema = Type.Object({
10
11
  action: ActionSchema,
11
12
  subject: Type.Optional(Type.String({
12
13
  description: "Short imperative task subject, required for create; use a reviewable unit of work, not a micro-step.",
13
14
  })),
14
- description: Type.Optional(Type.String({ description: "Longer task notes, acceptance criteria, or verification detail." })),
15
+ description: Type.Optional(Type.String({
16
+ description: "What done means for this task: notes, acceptance criteria, or verification detail. Required for create; on update an empty string removes it.",
17
+ })),
15
18
  activeForm: Type.Optional(Type.String({
16
- description: "Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'.",
19
+ description: "Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'; on update an empty string removes it.",
17
20
  })),
18
21
  status: Type.Optional(StatusSchema),
19
- blockedBy: Type.Optional(Type.Array(Type.Number(), {
20
- description: "Initial dependency ids for create; use only for real ordering constraints.",
22
+ blockedBy: Type.Optional(Type.Array(TaskId("Existing task id this task depends on."), {
23
+ description: "Initial dependency ids, create only (update rejects it; use addBlockedBy/removeBlockedBy there); use only for real ordering constraints.",
21
24
  })),
22
- addBlockedBy: Type.Optional(Type.Array(Type.Number(), {
25
+ addBlockedBy: Type.Optional(Type.Array(TaskId("Existing task id that must complete first."), {
23
26
  description: "Dependency ids to add on update; dependencies must exist and cannot create cycles.",
24
27
  })),
25
- removeBlockedBy: Type.Optional(Type.Array(Type.Number(), { description: "Dependency ids to remove on update." })),
26
- owner: Type.Optional(Type.String({ description: "Optional owner or agent label for multi-agent coordination." })),
28
+ removeBlockedBy: Type.Optional(Type.Array(TaskId("Dependency id to detach."), { description: "Dependency ids to remove on update." })),
29
+ addBlocks: Type.Optional(Type.Array(TaskId("Existing task id that must wait for this one."), {
30
+ description: "Update only: ids of tasks that cannot start until this task completes (the reverse of addBlockedBy); targets must exist and cannot create cycles.",
31
+ })),
32
+ removeBlocks: Type.Optional(Type.Array(TaskId("Dependent task id to release."), {
33
+ description: "Update only: ids of tasks that should no longer wait for this task.",
34
+ })),
35
+ owner: Type.Optional(Type.String({
36
+ description: "Optional owner or agent label for multi-agent coordination; on update an empty string removes it.",
37
+ })),
27
38
  metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
28
39
  description: "Optional structured metadata for integrations; null values delete keys on update.",
29
40
  })),
30
- id: Type.Optional(Type.Number({ description: "Task id, required for update, get, and delete." })),
41
+ id: Type.Optional(TaskId("Task id, required for update, get, and delete.")),
31
42
  includeDeleted: Type.Optional(Type.Boolean({
32
43
  description: "Include deleted tombstones in unfiltered list output; an explicit status=deleted query includes them automatically.",
33
44
  })),
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/todo/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AA6B5C,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,CAAU,EAAE;IAC5F,WAAW,EACV,oTAAoT;CACrT,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAU,EAAE;IAChG,WAAW,EACV,qOAAqO;CACtO,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EACV,sGAAsG;KACvG,CAAC,CACF;IACD,WAAW,EAAE,IAAI,CAAC,QAAQ,CACzB,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC,CAC/F;IACD,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iGAAiG;KAC9G,CAAC,CACF;IACD,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;IACnC,SAAS,EAAE,IAAI,CAAC,QAAQ,CACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;QACzB,WAAW,EAAE,4EAA4E;KACzF,CAAC,CACF;IACD,YAAY,EAAE,IAAI,CAAC,QAAQ,CAC1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;QACzB,WAAW,EAAE,oFAAoF;KACjG,CAAC,CACF;IACD,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC,CAAC;IACjH,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,QAAQ,CACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE;QAC1C,WAAW,EAAE,mFAAmF;KAChG,CAAC,CACF;IACD,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC,CAAC;IACjG,cAAc,EAAE,IAAI,CAAC,QAAQ,CAC5B,IAAI,CAAC,OAAO,CAAC;QACZ,WAAW,EACV,qHAAqH;KACtH,CAAC,CACF;CACD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAAc,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC","sourcesContent":["import { StringEnum } from \"@earendil-works/pi-ai\";\nimport { type Static, Type } from \"typebox\";\n\nexport type TodoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"deleted\";\nexport type TodoAction = \"create\" | \"update\" | \"list\" | \"get\" | \"delete\" | \"clear\";\n\nexport interface TodoItem {\n\tid: number;\n\tsubject: string;\n\tdescription?: string;\n\tactiveForm?: string;\n\tstatus: TodoStatus;\n\tblockedBy?: number[];\n\towner?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface TodoState {\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nexport interface TodoDetails {\n\taction: TodoAction;\n\tparams: Record<string, unknown>;\n\titems: TodoItem[];\n\tnextId: number;\n\terror?: string;\n}\n\nconst StatusSchema = StringEnum([\"pending\", \"in_progress\", \"completed\", \"deleted\"] as const, {\n\tdescription:\n\t\t\"Task status: pending for future work, in_progress for the single active task, completed for verified done work, and deleted for an immutable obsolete-task tombstone. Set deleted through update or use the delete action. With action list, status also acts as a filter: only tasks with this status are listed.\",\n});\n\nconst ActionSchema = StringEnum([\"create\", \"update\", \"list\", \"get\", \"delete\", \"clear\"] as const, {\n\tdescription:\n\t\t\"Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids.\",\n});\n\nexport const TodoParamsSchema = Type.Object({\n\taction: ActionSchema,\n\tsubject: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Short imperative task subject, required for create; use a reviewable unit of work, not a micro-step.\",\n\t\t}),\n\t),\n\tdescription: Type.Optional(\n\t\tType.String({ description: \"Longer task notes, acceptance criteria, or verification detail.\" }),\n\t),\n\tactiveForm: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'.\",\n\t\t}),\n\t),\n\tstatus: Type.Optional(StatusSchema),\n\tblockedBy: Type.Optional(\n\t\tType.Array(Type.Number(), {\n\t\t\tdescription: \"Initial dependency ids for create; use only for real ordering constraints.\",\n\t\t}),\n\t),\n\taddBlockedBy: Type.Optional(\n\t\tType.Array(Type.Number(), {\n\t\t\tdescription: \"Dependency ids to add on update; dependencies must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlockedBy: Type.Optional(Type.Array(Type.Number(), { description: \"Dependency ids to remove on update.\" })),\n\towner: Type.Optional(Type.String({ description: \"Optional owner or agent label for multi-agent coordination.\" })),\n\tmetadata: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Optional structured metadata for integrations; null values delete keys on update.\",\n\t\t}),\n\t),\n\tid: Type.Optional(Type.Number({ description: \"Task id, required for update, get, and delete.\" })),\n\tincludeDeleted: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Include deleted tombstones in unfiltered list output; an explicit status=deleted query includes them automatically.\",\n\t\t}),\n\t),\n});\n\nexport type TodoParams = Static<typeof TodoParamsSchema>;\n\nexport const EMPTY_TODO_STATE: TodoState = { items: [], nextId: 1 };\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/todo/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AA4B5C,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,CAAU,EAAE;IAC5F,WAAW,EACV,oTAAoT;CACrT,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAU,EAAE;IAChG,WAAW,EACV,qSAAqS;CACtS,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,CAAC,WAAmB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC;AAElF,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3C,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EACV,sGAAsG;KACvG,CAAC,CACF;IACD,WAAW,EAAE,IAAI,CAAC,QAAQ,CACzB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EACV,+IAA+I;KAChJ,CAAC,CACF;IACD,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EACV,uIAAuI;KACxI,CAAC,CACF;IACD,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;IACnC,SAAS,EAAE,IAAI,CAAC,QAAQ,CACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,wCAAwC,CAAC,EAAE;QAC5D,WAAW,EACV,0IAA0I;KAC3I,CAAC,CACF;IACD,YAAY,EAAE,IAAI,CAAC,QAAQ,CAC1B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,4CAA4C,CAAC,EAAE;QAChE,WAAW,EAAE,oFAAoF;KACjG,CAAC,CACF;IACD,eAAe,EAAE,IAAI,CAAC,QAAQ,CAC7B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,0BAA0B,CAAC,EAAE,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC,CACtG;IACD,SAAS,EAAE,IAAI,CAAC,QAAQ,CACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,+CAA+C,CAAC,EAAE;QACnE,WAAW,EACV,mJAAmJ;KACpJ,CAAC,CACF;IACD,YAAY,EAAE,IAAI,CAAC,QAAQ,CAC1B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,+BAA+B,CAAC,EAAE;QACnD,WAAW,EAAE,qEAAqE;KAClF,CAAC,CACF;IACD,KAAK,EAAE,IAAI,CAAC,QAAQ,CACnB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EACV,mGAAmG;KACpG,CAAC,CACF;IACD,QAAQ,EAAE,IAAI,CAAC,QAAQ,CACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE;QAC1C,WAAW,EAAE,mFAAmF;KAChG,CAAC,CACF;IACD,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,gDAAgD,CAAC,CAAC;IAC3E,cAAc,EAAE,IAAI,CAAC,QAAQ,CAC5B,IAAI,CAAC,OAAO,CAAC;QACZ,WAAW,EACV,qHAAqH;KACtH,CAAC,CACF;CACD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAAc,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC","sourcesContent":["import { StringEnum } from \"@earendil-works/pi-ai\";\nimport { type Static, Type } from \"typebox\";\n\nexport type TodoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"deleted\";\nexport type TodoAction = \"create\" | \"update\" | \"list\" | \"get\" | \"delete\" | \"clear\";\n\nexport interface TodoItem {\n\tid: number;\n\tsubject: string;\n\tdescription?: string;\n\tactiveForm?: string;\n\tstatus: TodoStatus;\n\tblockedBy?: number[];\n\towner?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface TodoState {\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nexport interface TodoDetails {\n\taction: TodoAction;\n\tparams: Record<string, unknown>;\n\titems: TodoItem[];\n\tnextId: number;\n}\n\nconst StatusSchema = StringEnum([\"pending\", \"in_progress\", \"completed\", \"deleted\"] as const, {\n\tdescription:\n\t\t\"Task status: pending for future work, in_progress for the single active task, completed for verified done work, and deleted for an immutable obsolete-task tombstone. Set deleted through update or use the delete action. With action list, status also acts as a filter: only tasks with this status are listed.\",\n});\n\nconst ActionSchema = StringEnum([\"create\", \"update\", \"list\", \"get\", \"delete\", \"clear\"] as const, {\n\tdescription:\n\t\t\"Todo operation: create a pending task, update status/details/dependencies, list current tasks (optionally filtered by status), get one task, delete an obsolete task and release dependents, or clear the list without reusing ids. Parameters that do not apply to the chosen action are rejected.\",\n});\n\nconst TaskId = (description: string) => Type.Integer({ minimum: 1, description });\n\nexport const TodoParamsSchema = Type.Object({\n\taction: ActionSchema,\n\tsubject: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Short imperative task subject, required for create; use a reviewable unit of work, not a micro-step.\",\n\t\t}),\n\t),\n\tdescription: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"What done means for this task: notes, acceptance criteria, or verification detail. Required for create; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tactiveForm: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Present-continuous label shown while in_progress, such as 'reading code' or 'updating prompts'; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tstatus: Type.Optional(StatusSchema),\n\tblockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id this task depends on.\"), {\n\t\t\tdescription:\n\t\t\t\t\"Initial dependency ids, create only (update rejects it; use addBlockedBy/removeBlockedBy there); use only for real ordering constraints.\",\n\t\t}),\n\t),\n\taddBlockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id that must complete first.\"), {\n\t\t\tdescription: \"Dependency ids to add on update; dependencies must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlockedBy: Type.Optional(\n\t\tType.Array(TaskId(\"Dependency id to detach.\"), { description: \"Dependency ids to remove on update.\" }),\n\t),\n\taddBlocks: Type.Optional(\n\t\tType.Array(TaskId(\"Existing task id that must wait for this one.\"), {\n\t\t\tdescription:\n\t\t\t\t\"Update only: ids of tasks that cannot start until this task completes (the reverse of addBlockedBy); targets must exist and cannot create cycles.\",\n\t\t}),\n\t),\n\tremoveBlocks: Type.Optional(\n\t\tType.Array(TaskId(\"Dependent task id to release.\"), {\n\t\t\tdescription: \"Update only: ids of tasks that should no longer wait for this task.\",\n\t\t}),\n\t),\n\towner: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional owner or agent label for multi-agent coordination; on update an empty string removes it.\",\n\t\t}),\n\t),\n\tmetadata: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Optional structured metadata for integrations; null values delete keys on update.\",\n\t\t}),\n\t),\n\tid: Type.Optional(TaskId(\"Task id, required for update, get, and delete.\")),\n\tincludeDeleted: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Include deleted tombstones in unfiltered list output; an explicit status=deleted query includes them automatically.\",\n\t\t}),\n\t),\n});\n\nexport type TodoParams = Static<typeof TodoParamsSchema>;\n\nexport const EMPTY_TODO_STATE: TodoState = { items: [], nextId: 1 };\n"]}
@@ -9,12 +9,15 @@ type Operation = {
9
9
  to: TodoStatus;
10
10
  /** Other tasks auto-demoted from in_progress → pending to keep exactly one active. */
11
11
  demotedIds?: number[];
12
+ /** Pending dependents left fully unblocked by an update to status deleted. */
13
+ releasedIds?: number[];
12
14
  /** Soft note when the list is fully closed without a verification-style task. */
13
15
  verificationNudge?: boolean;
14
16
  } | {
15
17
  kind: "delete";
16
18
  id: number;
17
19
  subject: string;
20
+ releasedIds?: number[];
18
21
  } | {
19
22
  kind: "list";
20
23
  status?: TodoStatus;
@@ -35,6 +38,7 @@ export interface MutationResult {
35
38
  }
36
39
  /** Point the module at a session's state bucket. Call wherever ctx is in hand. */
37
40
  export declare function setActiveTodoSession(sid: string): void;
41
+ /** Snapshot of the active session's state; mutate via applyTodoMutation + commitTodoState. */
38
42
  export declare function getTodoState(): TodoState;
39
43
  export declare function replaceTodoState(next: TodoState): void;
40
44
  export declare function commitTodoState(next: TodoState): void;
@@ -51,7 +55,7 @@ export declare function replayTodosFromBranch(ctx: {
51
55
  getBranch(): Iterable<unknown>;
52
56
  };
53
57
  }): TodoState;
54
- export declare function buildTodoDetails(params: TodoParams, next: TodoState, operation: Operation): TodoDetails;
58
+ export declare function buildTodoDetails(params: TodoParams, next: TodoState): TodoDetails;
55
59
  export declare function formatTodoContent(operation: Operation, state: TodoState): string;
56
60
  export {};
57
61
  //# sourceMappingURL=state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/state.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AAErB,KAAK,SAAS,GACX;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,UAAU,CAAC;IACf,wFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC3B,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAKtC,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;CACrB;AAWD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,SAAS,CAExC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAEtD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAErD;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAYvD;AA+DD,iFAAiF;AACjF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElF;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAE/E;AAyBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,CA4KtF;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE;IAAE,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;CAAE,GAAG,SAAS,CAiB5G;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,WAAW,CAQvG;AAKD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAgChF","sourcesContent":["import { LIST_DISPLAY_MAX_ITEMS, TODO_TOOL_NAME } from \"./constants.ts\";\nimport {\n\tEMPTY_TODO_STATE,\n\ttype TodoDetails,\n\ttype TodoItem,\n\ttype TodoParams,\n\ttype TodoState,\n\ttype TodoStatus,\n} from \"./schema.ts\";\n\ntype Operation =\n\t| { kind: \"create\"; id: number }\n\t| {\n\t\t\tkind: \"update\";\n\t\t\tid: number;\n\t\t\tfrom: TodoStatus;\n\t\t\tto: TodoStatus;\n\t\t\t/** Other tasks auto-demoted from in_progress → pending to keep exactly one active. */\n\t\t\tdemotedIds?: number[];\n\t\t\t/** Soft note when the list is fully closed without a verification-style task. */\n\t\t\tverificationNudge?: boolean;\n\t }\n\t| { kind: \"delete\"; id: number; subject: string }\n\t| { kind: \"list\"; status?: TodoStatus; includeDeleted: boolean }\n\t| { kind: \"get\"; item: TodoItem }\n\t| { kind: \"clear\"; count: number }\n\t| { kind: \"error\"; message: string };\n\n/** Subjects/descriptions that count as a verification step (CC-style soft nudge). */\nconst VERIFICATION_PATTERN = /verif|test|check|review/i;\n\nexport interface MutationResult {\n\tstate: TodoState;\n\toperation: Operation;\n}\n\n// ---- per-session state ------------------------------------------------------\n// Keyed by session id like rewind's engine state: one Pi process can host more\n// than one session over its lifetime (resume, /tree branch switches), and a\n// module-level singleton would leak one session's list into another. Tool\n// renderers get no ctx, so the active session is a module-level pointer kept\n// current by execute and the lifecycle handlers (which do have ctx).\nconst states = new Map<string, TodoState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActiveTodoSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getTodoState(): TodoState {\n\treturn states.get(activeSid) ?? cloneState(EMPTY_TODO_STATE);\n}\n\nexport function replaceTodoState(next: TodoState): void {\n\tstates.set(activeSid, cloneState(next));\n}\n\nexport function commitTodoState(next: TodoState): void {\n\treplaceTodoState(next);\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposeTodoSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function cloneState(source: TodoState): TodoState {\n\treturn {\n\t\titems: source.items.map((item) => {\n\t\t\tconst clone: TodoItem = { ...item };\n\t\t\tif (item.blockedBy) clone.blockedBy = [...item.blockedBy];\n\t\t\telse delete clone.blockedBy;\n\t\t\tif (item.metadata) clone.metadata = { ...item.metadata };\n\t\t\telse delete clone.metadata;\n\t\t\treturn clone;\n\t\t}),\n\t\tnextId: source.nextId,\n\t};\n}\n\nfunction omitBlockedBy(item: TodoItem): TodoItem {\n\tconst next = { ...item };\n\tdelete next.blockedBy;\n\treturn next;\n}\n\n/** Keep a tombstone while detaching its id from every dependent task. */\nfunction markDeleted(state: TodoState, id: number): TodoState {\n\tconst items = state.items.map((item) => {\n\t\tif (item.id === id) return { ...item, status: \"deleted\" as const };\n\t\tif (!item.blockedBy?.includes(id)) return item;\n\t\tconst blockedBy = item.blockedBy.filter((dependencyId) => dependencyId !== id);\n\t\treturn blockedBy.length ? { ...item, blockedBy } : omitBlockedBy(item);\n\t});\n\treturn { items, nextId: state.nextId };\n}\n\nfunction error(state: TodoState, message: string): MutationResult {\n\treturn { state, operation: { kind: \"error\", message } };\n}\n\nfunction isTransitionAllowed(from: TodoStatus, to: TodoStatus): boolean {\n\t// `deleted` is the only terminal status; `completed` can be reopened to\n\t// in_progress or pending so a premature completion recovers without losing\n\t// the task id and its blockedBy edges.\n\tif (from === to) return true;\n\tif (from === \"deleted\") return false;\n\tif (to === \"deleted\") return true;\n\tif (to === \"completed\") return from === \"pending\" || from === \"in_progress\";\n\tif (to === \"pending\") return from === \"in_progress\" || from === \"completed\";\n\tif (to === \"in_progress\") return from === \"pending\" || from === \"completed\";\n\treturn false;\n}\n\nfunction findItem(state: TodoState, id: number): TodoItem | undefined {\n\treturn state.items.find((item) => item.id === id);\n}\n\nfunction validateDependencies(state: TodoState, deps: number[] | undefined, currentId?: number): string | undefined {\n\tif (!deps?.length) return undefined;\n\tfor (const dep of deps) {\n\t\tif (dep === currentId) return `cannot block #${currentId} on itself`;\n\t\tconst item = findItem(state, dep);\n\t\tif (!item) return `blockedBy: #${dep} not found`;\n\t\tif (item.status === \"deleted\") return `blockedBy: #${dep} is deleted`;\n\t}\n\treturn undefined;\n}\n\n/** All listed dependencies must be completed before starting or closing a task. */\nfunction validateDependenciesReady(state: TodoState, deps: number[] | undefined): string | undefined {\n\tif (!deps?.length) return undefined;\n\tfor (const dep of deps) {\n\t\tconst item = findItem(state, dep);\n\t\tif (!item || item.status !== \"completed\") {\n\t\t\treturn `blockedBy: complete #${dep} before starting or finishing this task`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** IDs of dependencies that still prevent a task from starting or completing. */\nexport function unresolvedDependencyIds(state: TodoState, item: TodoItem): number[] {\n\treturn (item.blockedBy ?? []).filter((id) => findItem(state, id)?.status !== \"completed\");\n}\n\n/** True when every blockedBy dependency is completed (for transition and overlay checks). */\nexport function dependenciesSatisfied(state: TodoState, item: TodoItem): boolean {\n\treturn unresolvedDependencyIds(state, item).length === 0;\n}\n\nfunction createsCycle(items: TodoItem[], id: number, nextDeps: number[]): boolean {\n\tconst depsById = new Map<number, number[]>();\n\tfor (const item of items) depsById.set(item.id, item.blockedBy ?? []);\n\tdepsById.set(id, nextDeps);\n\n\tconst seen = new Set<number>();\n\tconst stack = new Set<number>();\n\n\tfunction visit(node: number): boolean {\n\t\tif (stack.has(node)) return true;\n\t\tif (seen.has(node)) return false;\n\t\tseen.add(node);\n\t\tstack.add(node);\n\t\tfor (const dep of depsById.get(node) ?? []) {\n\t\t\tif (visit(dep)) return true;\n\t\t}\n\t\tstack.delete(node);\n\t\treturn false;\n\t}\n\n\treturn visit(id);\n}\n\nexport function applyTodoMutation(input: TodoState, params: TodoParams): MutationResult {\n\tconst state = cloneState(input);\n\n\tswitch (params.action) {\n\t\tcase \"create\": {\n\t\t\tconst subject = params.subject?.trim();\n\t\t\tif (!subject) return error(state, \"subject required for create\");\n\t\t\tif (params.status !== undefined && params.status !== \"pending\") {\n\t\t\t\treturn error(state, \"create always starts pending; use update to start or complete a task\");\n\t\t\t}\n\t\t\tconst dependencyError = validateDependencies(state, params.blockedBy);\n\t\t\tif (dependencyError) return error(state, dependencyError);\n\n\t\t\tconst item: TodoItem = {\n\t\t\t\tid: state.nextId,\n\t\t\t\tsubject,\n\t\t\t\tstatus: \"pending\",\n\t\t\t};\n\t\t\tif (params.description) item.description = params.description;\n\t\t\tif (params.activeForm) item.activeForm = params.activeForm;\n\t\t\tif (params.blockedBy?.length) item.blockedBy = Array.from(new Set<number>(params.blockedBy));\n\t\t\tif (params.owner) item.owner = params.owner;\n\t\t\tif (params.metadata) item.metadata = { ...params.metadata };\n\n\t\t\treturn {\n\t\t\t\tstate: { items: [...state.items, item], nextId: state.nextId + 1 },\n\t\t\t\toperation: { kind: \"create\", id: item.id },\n\t\t\t};\n\t\t}\n\n\t\tcase \"update\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for update\");\n\t\t\tconst index = state.items.findIndex((item) => item.id === params.id);\n\t\t\tif (index === -1) return error(state, `#${params.id} not found`);\n\t\t\tconst current = state.items[index];\n\t\t\tif (current.status === \"deleted\") return error(state, `#${params.id} is deleted and cannot be updated`);\n\t\t\tif (params.subject !== undefined && !params.subject.trim()) return error(state, \"subject cannot be empty\");\n\n\t\t\tconst hasChange =\n\t\t\t\tparams.subject !== undefined ||\n\t\t\t\tparams.description !== undefined ||\n\t\t\t\tparams.activeForm !== undefined ||\n\t\t\t\tparams.status !== undefined ||\n\t\t\t\tparams.owner !== undefined ||\n\t\t\t\tparams.metadata !== undefined ||\n\t\t\t\t(params.addBlockedBy?.length ?? 0) > 0 ||\n\t\t\t\t(params.removeBlockedBy?.length ?? 0) > 0;\n\t\t\tif (!hasChange) return error(state, \"update requires at least one field\");\n\n\t\t\t// `status: deleted` is the update form of delete; keep its dependency\n\t\t\t// cleanup identical to the dedicated delete action.\n\t\t\tif (params.status === \"deleted\") {\n\t\t\t\treturn {\n\t\t\t\t\tstate: markDeleted(state, current.id),\n\t\t\t\t\toperation: { kind: \"update\", id: current.id, from: current.status, to: \"deleted\" },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst nextStatus = params.status ?? current.status;\n\t\t\tif (!isTransitionAllowed(current.status, nextStatus)) {\n\t\t\t\treturn error(state, `illegal transition ${current.status} -> ${nextStatus}`);\n\t\t\t}\n\n\t\t\tlet nextDeps = current.blockedBy ? [...current.blockedBy] : [];\n\t\t\tif (params.removeBlockedBy?.length) {\n\t\t\t\tconst remove = new Set(params.removeBlockedBy);\n\t\t\t\tnextDeps = nextDeps.filter((id) => !remove.has(id));\n\t\t\t}\n\t\t\tif (params.addBlockedBy?.length) {\n\t\t\t\tconst dependencyError = validateDependencies(state, params.addBlockedBy, current.id);\n\t\t\t\tif (dependencyError) return error(state, dependencyError);\n\t\t\t\tfor (const dep of params.addBlockedBy) {\n\t\t\t\t\tif (!nextDeps.includes(dep)) nextDeps.push(dep);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (createsCycle(state.items, current.id, nextDeps)) {\n\t\t\t\treturn error(state, \"blockedBy would create a cycle\");\n\t\t\t}\n\n\t\t\tif ((nextStatus === \"in_progress\" || nextStatus === \"completed\") && current.status !== nextStatus) {\n\t\t\t\tconst readyError = validateDependenciesReady(state, nextDeps);\n\t\t\t\tif (readyError) return error(state, readyError);\n\t\t\t}\n\n\t\t\tconst updated: TodoItem = { ...current, status: nextStatus };\n\t\t\tif (params.subject !== undefined) updated.subject = params.subject.trim();\n\t\t\tif (params.description !== undefined) updated.description = params.description;\n\t\t\tif (params.activeForm !== undefined) updated.activeForm = params.activeForm;\n\t\t\tif (params.owner !== undefined) updated.owner = params.owner;\n\t\t\tif (nextDeps.length) updated.blockedBy = nextDeps;\n\t\t\telse delete updated.blockedBy;\n\n\t\t\tif (params.metadata !== undefined) {\n\t\t\t\tconst merged = { ...(current.metadata ?? {}) };\n\t\t\t\tfor (const [key, value] of Object.entries(params.metadata)) {\n\t\t\t\t\tif (value === null) delete merged[key];\n\t\t\t\t\telse merged[key] = value;\n\t\t\t\t}\n\t\t\t\tif (Object.keys(merged).length) updated.metadata = merged;\n\t\t\t\telse delete updated.metadata;\n\t\t\t}\n\n\t\t\tconst items = [...state.items];\n\t\t\titems[index] = updated;\n\n\t\t\t// Exactly one in_progress: demote any other active tasks to pending.\n\t\t\tconst demotedIds: number[] = [];\n\t\t\tif (nextStatus === \"in_progress\") {\n\t\t\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\t\t\tconst item = items[i];\n\t\t\t\t\tif (item.id === updated.id) continue;\n\t\t\t\t\tif (item.status !== \"in_progress\") continue;\n\t\t\t\t\titems[i] = { ...item, status: \"pending\" };\n\t\t\t\t\tdemotedIds.push(item.id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextState: TodoState = { items, nextId: state.nextId };\n\t\t\tconst newlyCompleted = current.status !== \"completed\" && nextStatus === \"completed\";\n\t\t\treturn {\n\t\t\t\tstate: nextState,\n\t\t\t\toperation: {\n\t\t\t\t\tkind: \"update\",\n\t\t\t\t\tid: updated.id,\n\t\t\t\t\tfrom: current.status,\n\t\t\t\t\tto: updated.status,\n\t\t\t\t\t...(demotedIds.length ? { demotedIds } : {}),\n\t\t\t\t\t...(newlyCompleted && needsVerificationNudge(nextState) ? { verificationNudge: true } : {}),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tcase \"list\":\n\t\t\treturn {\n\t\t\t\tstate,\n\t\t\t\toperation: {\n\t\t\t\t\tkind: \"list\",\n\t\t\t\t\tstatus: params.status,\n\t\t\t\t\tincludeDeleted: params.includeDeleted === true,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase \"get\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for get\");\n\t\t\tconst item = findItem(state, params.id);\n\t\t\tif (!item) return error(state, `#${params.id} not found`);\n\t\t\treturn { state, operation: { kind: \"get\", item } };\n\t\t}\n\n\t\tcase \"delete\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for delete\");\n\t\t\tconst index = state.items.findIndex((item) => item.id === params.id);\n\t\t\tif (index === -1) return error(state, `#${params.id} not found`);\n\t\t\tconst current = state.items[index];\n\t\t\tif (current.status === \"deleted\") return error(state, `#${params.id} is already deleted`);\n\n\t\t\treturn {\n\t\t\t\tstate: markDeleted(state, current.id),\n\t\t\t\toperation: { kind: \"delete\", id: current.id, subject: current.subject },\n\t\t\t};\n\t\t}\n\n\t\tcase \"clear\":\n\t\t\treturn {\n\t\t\t\t// IDs remain monotonic across a clear so stale references cannot point\n\t\t\t\t// at an unrelated task created later on the same conversation branch.\n\t\t\t\tstate: { items: [], nextId: state.nextId },\n\t\t\t\toperation: { kind: \"clear\", count: state.items.length },\n\t\t\t};\n\t}\n\n\treturn error(state, `unknown action: ${(params as { action?: unknown }).action}`);\n}\n\nfunction isTodoDetails(value: unknown): value is TodoDetails {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn Array.isArray(record.items) && typeof record.nextId === \"number\";\n}\n\nexport function replayTodosFromBranch(ctx: { sessionManager: { getBranch(): Iterable<unknown> } }): TodoState {\n\t// Latest todo toolResult wins; scan tail → head so long branches stop early.\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as {\n\t\t\ttype?: string;\n\t\t\tmessage?: { role?: string; toolName?: string; details?: unknown };\n\t\t};\n\t\tif (item.type !== \"message\") continue;\n\t\tif (item.message?.role !== \"toolResult\" || item.message.toolName !== TODO_TOOL_NAME) continue;\n\t\tif (!isTodoDetails(item.message.details)) continue;\n\t\treturn cloneState({\n\t\t\titems: item.message.details.items,\n\t\t\tnextId: item.message.details.nextId,\n\t\t});\n\t}\n\treturn cloneState(EMPTY_TODO_STATE);\n}\n\nexport function buildTodoDetails(params: TodoParams, next: TodoState, operation: Operation): TodoDetails {\n\treturn {\n\t\taction: params.action,\n\t\tparams: params as Record<string, unknown>,\n\t\titems: cloneState(next).items,\n\t\tnextId: next.nextId,\n\t\t...(operation.kind === \"error\" ? { error: operation.message } : {}),\n\t};\n}\n\nconst VERIFICATION_NUDGE_NOTE =\n\t\"NOTE: You closed out 3+ tasks with no verification/test/review step. Before the final summary, run checks or add a verification task and complete it — do not self-declare done with known failures.\";\n\nexport function formatTodoContent(operation: Operation, state: TodoState): string {\n\tswitch (operation.kind) {\n\t\tcase \"create\": {\n\t\t\tconst item = findItem(state, operation.id);\n\t\t\treturn item ? `Created #${item.id}: ${item.subject} (pending)` : `Created #${operation.id}`;\n\t\t}\n\t\tcase \"update\": {\n\t\t\tconst transition = operation.from === operation.to ? \"\" : ` (${operation.from} -> ${operation.to})`;\n\t\t\tlet text = `Updated #${operation.id}${transition}`;\n\t\t\tif (operation.demotedIds?.length) {\n\t\t\t\ttext += `; ${operation.demotedIds.map((id) => `#${id}`).join(\",\")} demoted to pending`;\n\t\t\t}\n\t\t\tif (operation.verificationNudge) text += `\\n\\n${VERIFICATION_NUDGE_NOTE}`;\n\t\t\treturn text;\n\t\t}\n\t\tcase \"delete\":\n\t\t\treturn `Deleted #${operation.id}: ${operation.subject}`;\n\t\tcase \"clear\":\n\t\t\treturn `Cleared ${operation.count} tasks`;\n\t\tcase \"list\": {\n\t\t\tlet view = state.items;\n\t\t\t// An explicit deleted-status query is sufficient intent to include tombstones.\n\t\t\tif (!operation.includeDeleted && operation.status !== \"deleted\")\n\t\t\t\tview = view.filter((item) => item.status !== \"deleted\");\n\t\t\tif (operation.status) view = view.filter((item) => item.status === operation.status);\n\t\t\treturn formatBoundedList(view, operation.status, operation.includeDeleted);\n\t\t}\n\t\tcase \"get\":\n\t\t\treturn formatDetailItem(operation.item, state);\n\t\tcase \"error\":\n\t\t\treturn `Error: ${operation.message}`;\n\t}\n}\n\n/**\n * Soft nudge when the whole list is done (no active work), there are 3+ completed\n * tasks, and none look like a verification/test/review step — mirrors CC TodoWrite.\n */\nfunction needsVerificationNudge(state: TodoState): boolean {\n\tconst active = state.items.filter((item) => item.status !== \"deleted\");\n\tif (active.length < 3) return false;\n\tif (active.some((item) => item.status === \"pending\" || item.status === \"in_progress\")) return false;\n\tconst completed = active.filter((item) => item.status === \"completed\");\n\tif (completed.length < 3) return false;\n\treturn !completed.some(\n\t\t(item) =>\n\t\t\tVERIFICATION_PATTERN.test(item.subject) ||\n\t\t\t(item.description !== undefined && VERIFICATION_PATTERN.test(item.description)),\n\t);\n}\n\nfunction formatBoundedList(view: TodoItem[], statusFilter: TodoStatus | undefined, includeDeleted: boolean): string {\n\tif (!view.length) return \"No tasks\";\n\tconst shown = view.slice(0, LIST_DISPLAY_MAX_ITEMS);\n\tconst lines = shown.map(formatListItem);\n\tconst omitted = view.length - shown.length;\n\tif (omitted > 0) {\n\t\tconst filterHint = statusFilter ? `status=${statusFilter}` : includeDeleted ? \"includeDeleted=true\" : \"list\";\n\t\tlines.push(\n\t\t\t`… and ${omitted} more (${shown.length} of ${view.length} shown). Narrow with ${filterHint} or use get with id=.`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction formatListItem(item: TodoItem): string {\n\tconst deps = item.blockedBy?.length ? ` blockedBy=${item.blockedBy.map((id) => `#${id}`).join(\",\")}` : \"\";\n\tconst active = item.status === \"in_progress\" && item.activeForm ? ` (${item.activeForm})` : \"\";\n\treturn `[${item.status}] #${item.id} ${item.subject}${active}${deps}`;\n}\n\nfunction formatDetailItem(item: TodoItem, state: TodoState): string {\n\tconst lines = [`#${item.id} [${item.status}] ${item.subject}`];\n\tif (item.description) lines.push(`description: ${item.description}`);\n\tif (item.activeForm) lines.push(`activeForm: ${item.activeForm}`);\n\tif (item.blockedBy?.length) lines.push(`blockedBy: ${item.blockedBy.map((id) => `#${id}`).join(\", \")}`);\n\tconst blocks = state.items\n\t\t.filter((candidate) => candidate.blockedBy?.includes(item.id))\n\t\t.map((candidate) => candidate.id);\n\tif (blocks.length) lines.push(`blocks: ${blocks.map((id) => `#${id}`).join(\", \")}`);\n\tif (item.owner) lines.push(`owner: ${item.owner}`);\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/todo/state.ts"],"names":[],"mappings":"AACA,OAAO,EAGN,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,EACf,MAAM,aAAa,CAAC;AAErB,KAAK,SAAS,GACX;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,UAAU,CAAC;IACf,wFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC3B,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAKtC,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;CACrB;AAWD,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,8FAA8F;AAC9F,wBAAgB,YAAY,IAAI,SAAS,CAExC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAEtD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAErD;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAYvD;AAyHD,iFAAiF;AACjF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElF;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAE/E;AA6BD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,CAuPtF;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE;IAAE,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;CAAE,GAAG,SAAS,CAiB5G;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,CAOjF;AAUD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAiChF","sourcesContent":["import { LIST_DISPLAY_MAX_ITEMS, TODO_TOOL_NAME } from \"./constants.ts\";\nimport {\n\tEMPTY_TODO_STATE,\n\ttype TodoAction,\n\ttype TodoDetails,\n\ttype TodoItem,\n\ttype TodoParams,\n\ttype TodoState,\n\ttype TodoStatus,\n} from \"./schema.ts\";\n\ntype Operation =\n\t| { kind: \"create\"; id: number }\n\t| {\n\t\t\tkind: \"update\";\n\t\t\tid: number;\n\t\t\tfrom: TodoStatus;\n\t\t\tto: TodoStatus;\n\t\t\t/** Other tasks auto-demoted from in_progress → pending to keep exactly one active. */\n\t\t\tdemotedIds?: number[];\n\t\t\t/** Pending dependents left fully unblocked by an update to status deleted. */\n\t\t\treleasedIds?: number[];\n\t\t\t/** Soft note when the list is fully closed without a verification-style task. */\n\t\t\tverificationNudge?: boolean;\n\t }\n\t| { kind: \"delete\"; id: number; subject: string; releasedIds?: number[] }\n\t| { kind: \"list\"; status?: TodoStatus; includeDeleted: boolean }\n\t| { kind: \"get\"; item: TodoItem }\n\t| { kind: \"clear\"; count: number }\n\t| { kind: \"error\"; message: string };\n\n/** Subjects/descriptions that count as a verification step (CC-style soft nudge). */\nconst VERIFICATION_PATTERN = /verif|test|check|review/i;\n\nexport interface MutationResult {\n\tstate: TodoState;\n\toperation: Operation;\n}\n\n// ---- per-session state ------------------------------------------------------\n// Keyed by session id like rewind's engine state: one Pi process can host more\n// than one session over its lifetime (resume, /tree branch switches), and a\n// module-level singleton would leak one session's list into another. Tool\n// renderers get no ctx, so the active session is a module-level pointer kept\n// current by execute and the lifecycle handlers (which do have ctx).\nconst states = new Map<string, TodoState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActiveTodoSession(sid: string): void {\n\tactiveSid = sid;\n}\n\n/** Snapshot of the active session's state; mutate via applyTodoMutation + commitTodoState. */\nexport function getTodoState(): TodoState {\n\treturn cloneState(states.get(activeSid) ?? EMPTY_TODO_STATE);\n}\n\nexport function replaceTodoState(next: TodoState): void {\n\tstates.set(activeSid, cloneState(next));\n}\n\nexport function commitTodoState(next: TodoState): void {\n\treplaceTodoState(next);\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposeTodoSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function cloneState(source: TodoState): TodoState {\n\treturn {\n\t\titems: source.items.map((item) => {\n\t\t\tconst clone: TodoItem = { ...item };\n\t\t\tif (item.blockedBy) clone.blockedBy = [...item.blockedBy];\n\t\t\telse delete clone.blockedBy;\n\t\t\tif (item.metadata) clone.metadata = { ...item.metadata };\n\t\t\telse delete clone.metadata;\n\t\t\treturn clone;\n\t\t}),\n\t\tnextId: source.nextId,\n\t};\n}\n\nfunction omitBlockedBy(item: TodoItem): TodoItem {\n\tconst next = { ...item };\n\tdelete next.blockedBy;\n\treturn next;\n}\n\n/**\n * Keep a tombstone while detaching its id from every dependent task. Also\n * reports which pending dependents ended up fully unblocked, so the tool\n * result can tell the model what became workable (mirrors the demote note).\n */\nfunction markDeleted(state: TodoState, id: number): { state: TodoState; releasedIds: number[] } {\n\tconst items = state.items.map((item) => {\n\t\tif (item.id === id) return { ...item, status: \"deleted\" as const };\n\t\tif (!item.blockedBy?.includes(id)) return item;\n\t\tconst blockedBy = item.blockedBy.filter((dependencyId) => dependencyId !== id);\n\t\treturn blockedBy.length ? { ...item, blockedBy } : omitBlockedBy(item);\n\t});\n\tconst nextState: TodoState = { items, nextId: state.nextId };\n\tconst releasedIds = state.items\n\t\t.filter((item) => item.id !== id && item.status === \"pending\" && item.blockedBy?.includes(id))\n\t\t.map((item) => item.id)\n\t\t.filter((dependentId) => {\n\t\t\tconst dependent = findItem(nextState, dependentId);\n\t\t\treturn dependent !== undefined && unresolvedDependencyIds(nextState, dependent).length === 0;\n\t\t});\n\treturn { state: nextState, releasedIds };\n}\n\nfunction error(state: TodoState, message: string): MutationResult {\n\treturn { state, operation: { kind: \"error\", message } };\n}\n\n// ---- per-action parameter validation ---------------------------------------\n// Parameters each action accepts. Anything else is rejected with guidance\n// instead of being silently ignored (a model that passes blockedBy on update\n// should learn about addBlockedBy, not lose the edit).\nconst ACTION_PARAMS: Record<TodoAction, ReadonlySet<string>> = {\n\tcreate: new Set([\"subject\", \"description\", \"activeForm\", \"status\", \"blockedBy\", \"owner\", \"metadata\"]),\n\tupdate: new Set([\n\t\t\"id\",\n\t\t\"subject\",\n\t\t\"description\",\n\t\t\"activeForm\",\n\t\t\"status\",\n\t\t\"addBlockedBy\",\n\t\t\"removeBlockedBy\",\n\t\t\"addBlocks\",\n\t\t\"removeBlocks\",\n\t\t\"owner\",\n\t\t\"metadata\",\n\t]),\n\tlist: new Set([\"status\", \"includeDeleted\"]),\n\tget: new Set([\"id\"]),\n\tdelete: new Set([\"id\"]),\n\tclear: new Set<string>(),\n};\n\nconst UPDATE_ONLY_EDGE_PARAMS = new Set([\"addBlockedBy\", \"removeBlockedBy\", \"addBlocks\", \"removeBlocks\"]);\n\nfunction inapplicableParamError(action: TodoAction, key: string): string {\n\tif (action === \"update\" && key === \"blockedBy\") {\n\t\treturn \"blockedBy is create-only; use addBlockedBy/removeBlockedBy on update\";\n\t}\n\tif (action === \"create\" && UPDATE_ONLY_EDGE_PARAMS.has(key)) {\n\t\treturn `${key} is update-only; use blockedBy on create`;\n\t}\n\treturn `${key} does not apply to action ${action}`;\n}\n\nfunction findInapplicableParam(params: TodoParams): string | undefined {\n\tconst allowed = ACTION_PARAMS[params.action];\n\tfor (const [key, value] of Object.entries(params)) {\n\t\tif (key === \"action\" || value === undefined) continue;\n\t\tif (!allowed.has(key)) return inapplicableParamError(params.action, key);\n\t}\n\treturn undefined;\n}\n\nfunction isTransitionAllowed(from: TodoStatus, to: TodoStatus): boolean {\n\t// `deleted` is the only terminal status; `completed` can be reopened to\n\t// in_progress or pending so a premature completion recovers without losing\n\t// the task id and its blockedBy edges.\n\tif (from === to) return true;\n\tif (from === \"deleted\") return false;\n\tif (to === \"deleted\") return true;\n\tif (to === \"completed\") return from === \"pending\" || from === \"in_progress\";\n\tif (to === \"pending\") return from === \"in_progress\" || from === \"completed\";\n\tif (to === \"in_progress\") return from === \"pending\" || from === \"completed\";\n\treturn false;\n}\n\nfunction findItem(state: TodoState, id: number): TodoItem | undefined {\n\treturn state.items.find((item) => item.id === id);\n}\n\nfunction validateDependencies(state: TodoState, deps: number[] | undefined, currentId?: number): string | undefined {\n\tif (!deps?.length) return undefined;\n\tfor (const dep of deps) {\n\t\tif (dep === currentId) return `cannot block #${currentId} on itself`;\n\t\tconst item = findItem(state, dep);\n\t\tif (!item) return `blockedBy: #${dep} not found`;\n\t\tif (item.status === \"deleted\") return `blockedBy: #${dep} is deleted`;\n\t}\n\treturn undefined;\n}\n\n/** All listed dependencies must be completed before starting or closing a task. */\nfunction validateDependenciesReady(state: TodoState, deps: number[] | undefined): string | undefined {\n\tif (!deps?.length) return undefined;\n\tfor (const dep of deps) {\n\t\tconst item = findItem(state, dep);\n\t\tif (!item || item.status !== \"completed\") {\n\t\t\treturn `blockedBy: complete #${dep} before starting or finishing this task`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** IDs of dependencies that still prevent a task from starting or completing. */\nexport function unresolvedDependencyIds(state: TodoState, item: TodoItem): number[] {\n\treturn (item.blockedBy ?? []).filter((id) => findItem(state, id)?.status !== \"completed\");\n}\n\n/** True when every blockedBy dependency is completed (for transition and overlay checks). */\nexport function dependenciesSatisfied(state: TodoState, item: TodoItem): boolean {\n\treturn unresolvedDependencyIds(state, item).length === 0;\n}\n\n/** True when any task whose deps change in nextDepsById can reach itself through blockedBy edges. */\nfunction createsCycle(items: TodoItem[], nextDepsById: ReadonlyMap<number, number[]>): boolean {\n\tconst depsById = new Map<number, number[]>();\n\tfor (const item of items) depsById.set(item.id, item.blockedBy ?? []);\n\tfor (const [id, deps] of nextDepsById) depsById.set(id, deps);\n\n\tconst seen = new Set<number>();\n\tconst stack = new Set<number>();\n\n\tfunction visit(node: number): boolean {\n\t\tif (stack.has(node)) return true;\n\t\tif (seen.has(node)) return false;\n\t\tseen.add(node);\n\t\tstack.add(node);\n\t\tfor (const dep of depsById.get(node) ?? []) {\n\t\t\tif (visit(dep)) return true;\n\t\t}\n\t\tstack.delete(node);\n\t\treturn false;\n\t}\n\n\tfor (const id of nextDepsById.keys()) {\n\t\tif (visit(id)) return true;\n\t}\n\treturn false;\n}\n\nexport function applyTodoMutation(input: TodoState, params: TodoParams): MutationResult {\n\tconst state = cloneState(input);\n\n\tconst inapplicable = findInapplicableParam(params);\n\tif (inapplicable) return error(state, inapplicable);\n\n\tswitch (params.action) {\n\t\tcase \"create\": {\n\t\t\tconst subject = params.subject?.trim();\n\t\t\tif (!subject) return error(state, \"subject required for create\");\n\t\t\tif (!params.description?.trim()) {\n\t\t\t\treturn error(state, \"description required for create: state what done means for this task\");\n\t\t\t}\n\t\t\tif (params.status !== undefined && params.status !== \"pending\") {\n\t\t\t\treturn error(state, \"create always starts pending; use update to start or complete a task\");\n\t\t\t}\n\t\t\tconst dependencyError = validateDependencies(state, params.blockedBy);\n\t\t\tif (dependencyError) return error(state, dependencyError);\n\n\t\t\tconst item: TodoItem = {\n\t\t\t\tid: state.nextId,\n\t\t\t\tsubject,\n\t\t\t\tstatus: \"pending\",\n\t\t\t\tdescription: params.description,\n\t\t\t};\n\t\t\tif (params.activeForm) item.activeForm = params.activeForm;\n\t\t\tif (params.blockedBy?.length) item.blockedBy = Array.from(new Set<number>(params.blockedBy));\n\t\t\tif (params.owner) item.owner = params.owner;\n\t\t\tif (params.metadata) item.metadata = { ...params.metadata };\n\n\t\t\treturn {\n\t\t\t\tstate: { items: [...state.items, item], nextId: state.nextId + 1 },\n\t\t\t\toperation: { kind: \"create\", id: item.id },\n\t\t\t};\n\t\t}\n\n\t\tcase \"update\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for update\");\n\t\t\tconst index = state.items.findIndex((item) => item.id === params.id);\n\t\t\tif (index === -1) return error(state, `#${params.id} not found`);\n\t\t\tconst current = state.items[index];\n\t\t\tif (current.status === \"deleted\") return error(state, `#${params.id} is deleted and cannot be updated`);\n\t\t\tif (params.subject !== undefined && !params.subject.trim()) return error(state, \"subject cannot be empty\");\n\n\t\t\tconst edgeChange =\n\t\t\t\t(params.addBlockedBy?.length ?? 0) > 0 ||\n\t\t\t\t(params.removeBlockedBy?.length ?? 0) > 0 ||\n\t\t\t\t(params.addBlocks?.length ?? 0) > 0 ||\n\t\t\t\t(params.removeBlocks?.length ?? 0) > 0;\n\t\t\tconst fieldChange =\n\t\t\t\tparams.subject !== undefined ||\n\t\t\t\tparams.description !== undefined ||\n\t\t\t\tparams.activeForm !== undefined ||\n\t\t\t\tparams.owner !== undefined ||\n\t\t\t\tparams.metadata !== undefined;\n\t\t\tif (!fieldChange && !edgeChange && params.status === undefined) {\n\t\t\t\treturn error(state, \"update requires at least one field\");\n\t\t\t}\n\n\t\t\t// `status: deleted` is the update form of delete; keep its dependency\n\t\t\t// cleanup identical to the dedicated delete action. Other edits do not\n\t\t\t// combine with it: a tombstone is immutable, so applying them would\n\t\t\t// silently produce an unreachable revision.\n\t\t\tif (params.status === \"deleted\") {\n\t\t\t\tif (fieldChange || edgeChange) {\n\t\t\t\t\treturn error(\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\t\"status deleted cannot be combined with other changes; apply edits first or delete in a separate call\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst deleted = markDeleted(state, current.id);\n\t\t\t\treturn {\n\t\t\t\t\tstate: deleted.state,\n\t\t\t\t\toperation: {\n\t\t\t\t\t\tkind: \"update\",\n\t\t\t\t\t\tid: current.id,\n\t\t\t\t\t\tfrom: current.status,\n\t\t\t\t\t\tto: \"deleted\",\n\t\t\t\t\t\t...(deleted.releasedIds.length ? { releasedIds: deleted.releasedIds } : {}),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst nextStatus = params.status ?? current.status;\n\t\t\tif (!isTransitionAllowed(current.status, nextStatus)) {\n\t\t\t\treturn error(state, `illegal transition ${current.status} -> ${nextStatus}`);\n\t\t\t}\n\n\t\t\t// Collect every task whose blockedBy changes: this task (addBlockedBy /\n\t\t\t// removeBlockedBy) plus reverse-edge targets (addBlocks / removeBlocks).\n\t\t\tconst nextDepsById = new Map<number, number[]>();\n\t\t\tlet nextDeps = current.blockedBy ? [...current.blockedBy] : [];\n\t\t\tif (params.removeBlockedBy?.length) {\n\t\t\t\tconst remove = new Set(params.removeBlockedBy);\n\t\t\t\tnextDeps = nextDeps.filter((id) => !remove.has(id));\n\t\t\t}\n\t\t\tif (params.addBlockedBy?.length) {\n\t\t\t\tconst dependencyError = validateDependencies(state, params.addBlockedBy, current.id);\n\t\t\t\tif (dependencyError) return error(state, dependencyError);\n\t\t\t\tfor (const dep of params.addBlockedBy) {\n\t\t\t\t\tif (!nextDeps.includes(dep)) nextDeps.push(dep);\n\t\t\t\t}\n\t\t\t}\n\t\t\tnextDepsById.set(current.id, nextDeps);\n\n\t\t\tif (params.removeBlocks?.length) {\n\t\t\t\tconst release = new Set(params.removeBlocks);\n\t\t\t\tfor (const candidate of state.items) {\n\t\t\t\t\tif (!release.has(candidate.id) || !candidate.blockedBy?.includes(current.id)) continue;\n\t\t\t\t\tnextDepsById.set(\n\t\t\t\t\t\tcandidate.id,\n\t\t\t\t\t\tcandidate.blockedBy.filter((dependencyId) => dependencyId !== current.id),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (params.addBlocks?.length) {\n\t\t\t\tfor (const targetId of params.addBlocks) {\n\t\t\t\t\tif (targetId === current.id) return error(state, `cannot block #${targetId} on itself`);\n\t\t\t\t\tconst target = findItem(state, targetId);\n\t\t\t\t\tif (!target) return error(state, `addBlocks: #${targetId} not found`);\n\t\t\t\t\tif (target.status === \"deleted\") return error(state, `addBlocks: #${targetId} is deleted`);\n\t\t\t\t\tconst deps = nextDepsById.get(targetId) ?? (target.blockedBy ? [...target.blockedBy] : []);\n\t\t\t\t\tif (!deps.includes(current.id)) deps.push(current.id);\n\t\t\t\t\tnextDepsById.set(targetId, deps);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (createsCycle(state.items, nextDepsById)) {\n\t\t\t\treturn error(state, \"blockedBy would create a cycle\");\n\t\t\t}\n\n\t\t\t// Readiness gates only status *changes*: an already-active task may take\n\t\t\t// on a new incomplete dependency — a prerequisite discovered mid-work.\n\t\t\t// The overlay flags it as \"deps incomplete\" instead.\n\t\t\tif ((nextStatus === \"in_progress\" || nextStatus === \"completed\") && current.status !== nextStatus) {\n\t\t\t\tconst readyError = validateDependenciesReady(state, nextDeps);\n\t\t\t\tif (readyError) return error(state, readyError);\n\t\t\t}\n\n\t\t\tconst updated: TodoItem = { ...current, status: nextStatus };\n\t\t\tif (params.subject !== undefined) updated.subject = params.subject.trim();\n\t\t\t// Empty string removes an optional text field (metadata uses null keys).\n\t\t\tif (params.description !== undefined) {\n\t\t\t\tif (params.description) updated.description = params.description;\n\t\t\t\telse delete updated.description;\n\t\t\t}\n\t\t\tif (params.activeForm !== undefined) {\n\t\t\t\tif (params.activeForm) updated.activeForm = params.activeForm;\n\t\t\t\telse delete updated.activeForm;\n\t\t\t}\n\t\t\tif (params.owner !== undefined) {\n\t\t\t\tif (params.owner) updated.owner = params.owner;\n\t\t\t\telse delete updated.owner;\n\t\t\t}\n\t\t\tif (nextDeps.length) updated.blockedBy = nextDeps;\n\t\t\telse delete updated.blockedBy;\n\n\t\t\tif (params.metadata !== undefined) {\n\t\t\t\tconst merged = { ...(current.metadata ?? {}) };\n\t\t\t\tfor (const [key, value] of Object.entries(params.metadata)) {\n\t\t\t\t\tif (value === null) delete merged[key];\n\t\t\t\t\telse merged[key] = value;\n\t\t\t\t}\n\t\t\t\tif (Object.keys(merged).length) updated.metadata = merged;\n\t\t\t\telse delete updated.metadata;\n\t\t\t}\n\n\t\t\tconst items = state.items.map((task) => {\n\t\t\t\tif (task.id === current.id) return task;\n\t\t\t\tconst deps = nextDepsById.get(task.id);\n\t\t\t\tif (deps === undefined) return task;\n\t\t\t\treturn deps.length ? { ...task, blockedBy: deps } : omitBlockedBy(task);\n\t\t\t});\n\t\t\titems[index] = updated;\n\n\t\t\t// Exactly one in_progress: demote any other active tasks to pending.\n\t\t\tconst demotedIds: number[] = [];\n\t\t\tif (nextStatus === \"in_progress\") {\n\t\t\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\t\t\tconst item = items[i];\n\t\t\t\t\tif (item.id === updated.id) continue;\n\t\t\t\t\tif (item.status !== \"in_progress\") continue;\n\t\t\t\t\titems[i] = { ...item, status: \"pending\" };\n\t\t\t\t\tdemotedIds.push(item.id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextState: TodoState = { items, nextId: state.nextId };\n\t\t\tconst newlyCompleted = current.status !== \"completed\" && nextStatus === \"completed\";\n\t\t\treturn {\n\t\t\t\tstate: nextState,\n\t\t\t\toperation: {\n\t\t\t\t\tkind: \"update\",\n\t\t\t\t\tid: updated.id,\n\t\t\t\t\tfrom: current.status,\n\t\t\t\t\tto: updated.status,\n\t\t\t\t\t...(demotedIds.length ? { demotedIds } : {}),\n\t\t\t\t\t...(newlyCompleted && needsVerificationNudge(nextState) ? { verificationNudge: true } : {}),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tcase \"list\":\n\t\t\treturn {\n\t\t\t\tstate,\n\t\t\t\toperation: {\n\t\t\t\t\tkind: \"list\",\n\t\t\t\t\tstatus: params.status,\n\t\t\t\t\tincludeDeleted: params.includeDeleted === true,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase \"get\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for get\");\n\t\t\tconst item = findItem(state, params.id);\n\t\t\tif (!item) return error(state, `#${params.id} not found`);\n\t\t\treturn { state, operation: { kind: \"get\", item } };\n\t\t}\n\n\t\tcase \"delete\": {\n\t\t\tif (params.id === undefined) return error(state, \"id required for delete\");\n\t\t\tconst index = state.items.findIndex((item) => item.id === params.id);\n\t\t\tif (index === -1) return error(state, `#${params.id} not found`);\n\t\t\tconst current = state.items[index];\n\t\t\tif (current.status === \"deleted\") return error(state, `#${params.id} is already deleted`);\n\n\t\t\tconst deleted = markDeleted(state, current.id);\n\t\t\treturn {\n\t\t\t\tstate: deleted.state,\n\t\t\t\toperation: {\n\t\t\t\t\tkind: \"delete\",\n\t\t\t\t\tid: current.id,\n\t\t\t\t\tsubject: current.subject,\n\t\t\t\t\t...(deleted.releasedIds.length ? { releasedIds: deleted.releasedIds } : {}),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tcase \"clear\":\n\t\t\treturn {\n\t\t\t\t// IDs remain monotonic across a clear so stale references cannot point\n\t\t\t\t// at an unrelated task created later on the same conversation branch.\n\t\t\t\tstate: { items: [], nextId: state.nextId },\n\t\t\t\toperation: { kind: \"clear\", count: state.items.length },\n\t\t\t};\n\t}\n\n\treturn error(state, `unknown action: ${(params as { action?: unknown }).action}`);\n}\n\nfunction isTodoDetails(value: unknown): value is TodoDetails {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn Array.isArray(record.items) && typeof record.nextId === \"number\";\n}\n\nexport function replayTodosFromBranch(ctx: { sessionManager: { getBranch(): Iterable<unknown> } }): TodoState {\n\t// Latest todo toolResult wins; scan tail → head so long branches stop early.\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as {\n\t\t\ttype?: string;\n\t\t\tmessage?: { role?: string; toolName?: string; details?: unknown };\n\t\t};\n\t\tif (item.type !== \"message\") continue;\n\t\tif (item.message?.role !== \"toolResult\" || item.message.toolName !== TODO_TOOL_NAME) continue;\n\t\tif (!isTodoDetails(item.message.details)) continue;\n\t\treturn cloneState({\n\t\t\titems: item.message.details.items,\n\t\t\tnextId: item.message.details.nextId,\n\t\t});\n\t}\n\treturn cloneState(EMPTY_TODO_STATE);\n}\n\nexport function buildTodoDetails(params: TodoParams, next: TodoState): TodoDetails {\n\treturn {\n\t\taction: params.action,\n\t\tparams: params as Record<string, unknown>,\n\t\titems: cloneState(next).items,\n\t\tnextId: next.nextId,\n\t};\n}\n\nconst VERIFICATION_NUDGE_NOTE =\n\t\"NOTE: You closed out 3+ tasks with no verification/test/review step. Before the final summary, run checks or add a verification task and complete it — do not self-declare done with known failures.\";\n\nfunction formatReleasedSuffix(releasedIds: number[] | undefined): string {\n\tif (!releasedIds?.length) return \"\";\n\treturn `; released ${releasedIds.map((id) => `#${id}`).join(\",\")} (no longer blocked)`;\n}\n\nexport function formatTodoContent(operation: Operation, state: TodoState): string {\n\tswitch (operation.kind) {\n\t\tcase \"create\": {\n\t\t\tconst item = findItem(state, operation.id);\n\t\t\treturn item ? `Created #${item.id}: ${item.subject} (pending)` : `Created #${operation.id}`;\n\t\t}\n\t\tcase \"update\": {\n\t\t\tconst transition = operation.from === operation.to ? \"\" : ` (${operation.from} -> ${operation.to})`;\n\t\t\tlet text = `Updated #${operation.id}${transition}`;\n\t\t\tif (operation.demotedIds?.length) {\n\t\t\t\ttext += `; ${operation.demotedIds.map((id) => `#${id}`).join(\",\")} demoted to pending`;\n\t\t\t}\n\t\t\ttext += formatReleasedSuffix(operation.releasedIds);\n\t\t\tif (operation.verificationNudge) text += `\\n\\n${VERIFICATION_NUDGE_NOTE}`;\n\t\t\treturn text;\n\t\t}\n\t\tcase \"delete\":\n\t\t\treturn `Deleted #${operation.id}: ${operation.subject}${formatReleasedSuffix(operation.releasedIds)}`;\n\t\tcase \"clear\":\n\t\t\treturn `Cleared ${operation.count} tasks`;\n\t\tcase \"list\": {\n\t\t\tlet view = state.items;\n\t\t\t// An explicit deleted-status query is sufficient intent to include tombstones.\n\t\t\tif (!operation.includeDeleted && operation.status !== \"deleted\")\n\t\t\t\tview = view.filter((item) => item.status !== \"deleted\");\n\t\t\tif (operation.status) view = view.filter((item) => item.status === operation.status);\n\t\t\treturn formatBoundedList(view, state, operation.status);\n\t\t}\n\t\tcase \"get\":\n\t\t\treturn formatDetailItem(operation.item, state);\n\t\tcase \"error\":\n\t\t\treturn `Error: ${operation.message}`;\n\t}\n}\n\n/**\n * Soft nudge when the whole list is done (no active work), there are 3+ completed\n * tasks, and none look like a verification/test/review step — mirrors CC TodoWrite.\n */\nfunction needsVerificationNudge(state: TodoState): boolean {\n\tconst active = state.items.filter((item) => item.status !== \"deleted\");\n\tif (active.length < 3) return false;\n\tif (active.some((item) => item.status === \"pending\" || item.status === \"in_progress\")) return false;\n\tconst completed = active.filter((item) => item.status === \"completed\");\n\tif (completed.length < 3) return false;\n\treturn !completed.some(\n\t\t(item) =>\n\t\t\tVERIFICATION_PATTERN.test(item.subject) ||\n\t\t\t(item.description !== undefined && VERIFICATION_PATTERN.test(item.description)),\n\t);\n}\n\nfunction formatBoundedList(view: TodoItem[], state: TodoState, statusFilter: TodoStatus | undefined): string {\n\tif (!view.length) return \"No tasks\";\n\tconst shown = view.slice(0, LIST_DISPLAY_MAX_ITEMS);\n\tconst lines = shown.map((item) => formatListItem(item, state));\n\tconst omitted = view.length - shown.length;\n\tif (omitted > 0) {\n\t\t// Do not suggest a filter that is already applied.\n\t\tconst hint = statusFilter ? \"use get with id= for details\" : \"narrow with status= or use get with id=\";\n\t\tlines.push(`… and ${omitted} more (${shown.length} of ${view.length} shown); ${hint}.`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction formatListItem(item: TodoItem, state: TodoState): string {\n\tconst unresolved = unresolvedDependencyIds(state, item);\n\tconst deps = unresolved.length ? ` blockedBy=${unresolved.map((id) => `#${id}`).join(\",\")}` : \"\";\n\tconst owner = item.owner ? ` @${item.owner}` : \"\";\n\tconst active = item.status === \"in_progress\" && item.activeForm ? ` (${item.activeForm})` : \"\";\n\treturn `[${item.status}] #${item.id} ${item.subject}${active}${owner}${deps}`;\n}\n\nfunction formatDetailItem(item: TodoItem, state: TodoState): string {\n\tconst lines = [`#${item.id} [${item.status}] ${item.subject}`];\n\tif (item.description) lines.push(`description: ${item.description}`);\n\tif (item.activeForm) lines.push(`activeForm: ${item.activeForm}`);\n\tif (item.blockedBy?.length) lines.push(`blockedBy: ${item.blockedBy.map((id) => `#${id}`).join(\", \")}`);\n\tconst blocks = state.items\n\t\t.filter((candidate) => candidate.blockedBy?.includes(item.id))\n\t\t.map((candidate) => candidate.id);\n\tif (blocks.length) lines.push(`blocks: ${blocks.map((id) => `#${id}`).join(\", \")}`);\n\tif (item.owner) lines.push(`owner: ${item.owner}`);\n\treturn lines.join(\"\\n\");\n}\n"]}