@pentoshi/clai 3.6.0 → 3.7.2

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 (314) hide show
  1. package/dist/agent/compaction-summary.d.ts +17 -0
  2. package/dist/agent/compaction-summary.js +64 -0
  3. package/dist/agent/compaction-summary.js.map +1 -0
  4. package/dist/agent/confirm-port.d.ts +3 -1
  5. package/dist/agent/confirm-port.js +5 -1
  6. package/dist/agent/confirm-port.js.map +1 -1
  7. package/dist/agent/context-manager.js +45 -22
  8. package/dist/agent/context-manager.js.map +1 -1
  9. package/dist/agent/events.d.ts +6 -0
  10. package/dist/agent/evidence-governor.d.ts +37 -0
  11. package/dist/agent/evidence-governor.js +70 -0
  12. package/dist/agent/evidence-governor.js.map +1 -0
  13. package/dist/agent/loop-guard.d.ts +29 -11
  14. package/dist/agent/loop-guard.js +93 -39
  15. package/dist/agent/loop-guard.js.map +1 -1
  16. package/dist/agent/must-continue.d.ts +56 -0
  17. package/dist/agent/must-continue.js +240 -0
  18. package/dist/agent/must-continue.js.map +1 -0
  19. package/dist/agent/outcomes.d.ts +101 -0
  20. package/dist/agent/outcomes.js +359 -0
  21. package/dist/agent/outcomes.js.map +1 -0
  22. package/dist/agent/plan-decision.d.ts +33 -0
  23. package/dist/agent/plan-decision.js +104 -0
  24. package/dist/agent/plan-decision.js.map +1 -0
  25. package/dist/agent/plan-tool.d.ts +12 -0
  26. package/dist/agent/plan-tool.js +309 -104
  27. package/dist/agent/plan-tool.js.map +1 -1
  28. package/dist/agent/project-root.js +3 -0
  29. package/dist/agent/project-root.js.map +1 -1
  30. package/dist/agent/prompt-composer.d.ts +21 -0
  31. package/dist/agent/prompt-composer.js +46 -0
  32. package/dist/agent/prompt-composer.js.map +1 -0
  33. package/dist/agent/runner.d.ts +11 -1
  34. package/dist/agent/runner.js +1115 -965
  35. package/dist/agent/runner.js.map +1 -1
  36. package/dist/agent/scope-context.d.ts +3 -0
  37. package/dist/agent/scope-context.js +24 -0
  38. package/dist/agent/scope-context.js.map +1 -0
  39. package/dist/agent/scratch-write.d.ts +4 -0
  40. package/dist/agent/scratch-write.js +54 -0
  41. package/dist/agent/scratch-write.js.map +1 -0
  42. package/dist/agent/session-policy.d.ts +7 -0
  43. package/dist/agent/session-policy.js +61 -0
  44. package/dist/agent/session-policy.js.map +1 -1
  45. package/dist/agent/session-state.d.ts +41 -0
  46. package/dist/agent/session-state.js +107 -0
  47. package/dist/agent/session-state.js.map +1 -0
  48. package/dist/agent/step-budget.d.ts +14 -0
  49. package/dist/agent/step-budget.js +26 -0
  50. package/dist/agent/step-budget.js.map +1 -0
  51. package/dist/agent/task-analyzer.d.ts +12 -3
  52. package/dist/agent/task-analyzer.js +263 -23
  53. package/dist/agent/task-analyzer.js.map +1 -1
  54. package/dist/agent/task-evidence.d.ts +99 -29
  55. package/dist/agent/task-evidence.js +480 -91
  56. package/dist/agent/task-evidence.js.map +1 -1
  57. package/dist/agent/task-plan.d.ts +57 -0
  58. package/dist/agent/task-plan.js +147 -1
  59. package/dist/agent/task-plan.js.map +1 -1
  60. package/dist/agent/tool-call-parser.d.ts +4 -12
  61. package/dist/agent/tool-call-parser.js +34 -67
  62. package/dist/agent/tool-call-parser.js.map +1 -1
  63. package/dist/agent/tool-history.d.ts +7 -0
  64. package/dist/agent/tool-history.js +50 -0
  65. package/dist/agent/tool-history.js.map +1 -1
  66. package/dist/agent/tool-output-formatting.d.ts +13 -1
  67. package/dist/agent/tool-output-formatting.js +75 -22
  68. package/dist/agent/tool-output-formatting.js.map +1 -1
  69. package/dist/agent/turn-outcome.d.ts +11 -0
  70. package/dist/agent/turn-outcome.js +17 -0
  71. package/dist/agent/turn-outcome.js.map +1 -0
  72. package/dist/agent/turn-state.d.ts +11 -0
  73. package/dist/agent/turn-state.js +28 -0
  74. package/dist/agent/turn-state.js.map +1 -0
  75. package/dist/agent/workspace-orient.d.ts +11 -0
  76. package/dist/agent/workspace-orient.js +103 -12
  77. package/dist/agent/workspace-orient.js.map +1 -1
  78. package/dist/app/adapters/agent-event-adapter.d.ts +2 -13
  79. package/dist/app/adapters/agent-event-adapter.js +63 -25
  80. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  81. package/dist/app/adapters/current-agent-adapter.d.ts +0 -5
  82. package/dist/app/adapters/current-agent-adapter.js +4 -7
  83. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  84. package/dist/app/adapters/current-jobs-adapter.js +1 -0
  85. package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
  86. package/dist/app/adapters/current-terminal-adapter.d.ts +0 -7
  87. package/dist/app/adapters/current-terminal-adapter.js +0 -7
  88. package/dist/app/adapters/current-terminal-adapter.js.map +1 -1
  89. package/dist/app/adapters/current-updates-adapter.d.ts +0 -5
  90. package/dist/app/adapters/current-updates-adapter.js +0 -5
  91. package/dist/app/adapters/current-updates-adapter.js.map +1 -1
  92. package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +0 -5
  93. package/dist/app/adapters/in-memory-clipboard-adapter.js +0 -5
  94. package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -1
  95. package/dist/app/commands/command.d.ts +0 -5
  96. package/dist/app/commands/command.js.map +1 -1
  97. package/dist/app/commands/registry.d.ts +0 -14
  98. package/dist/app/commands/registry.js +0 -20
  99. package/dist/app/commands/registry.js.map +1 -1
  100. package/dist/app/controllers/disposable.d.ts +0 -5
  101. package/dist/app/controllers/disposable.js +0 -5
  102. package/dist/app/controllers/disposable.js.map +1 -1
  103. package/dist/app/controllers/job-controller.d.ts +1 -5
  104. package/dist/app/controllers/job-controller.js +0 -4
  105. package/dist/app/controllers/job-controller.js.map +1 -1
  106. package/dist/app/controllers/plan-controller.d.ts +5 -8
  107. package/dist/app/controllers/plan-controller.js +30 -12
  108. package/dist/app/controllers/plan-controller.js.map +1 -1
  109. package/dist/app/controllers/session-controller.d.ts +17 -32
  110. package/dist/app/controllers/session-controller.js +56 -35
  111. package/dist/app/controllers/session-controller.js.map +1 -1
  112. package/dist/app/controllers/turn-controller.d.ts +2 -6
  113. package/dist/app/controllers/turn-controller.js +8 -8
  114. package/dist/app/controllers/turn-controller.js.map +1 -1
  115. package/dist/app/events/app-event.d.ts +4 -7
  116. package/dist/app/events/app-event.js +0 -6
  117. package/dist/app/events/app-event.js.map +1 -1
  118. package/dist/app/events/event-buffer.d.ts +6 -17
  119. package/dist/app/events/event-buffer.js +27 -24
  120. package/dist/app/events/event-buffer.js.map +1 -1
  121. package/dist/app/events/sequencer.d.ts +0 -9
  122. package/dist/app/events/sequencer.js +0 -4
  123. package/dist/app/events/sequencer.js.map +1 -1
  124. package/dist/app/ports/agent-port.d.ts +13 -3
  125. package/dist/app/ports/clipboard-port.d.ts +0 -5
  126. package/dist/app/ports/confirm-port.d.ts +0 -7
  127. package/dist/app/ports/jobs-port.d.ts +2 -5
  128. package/dist/app/ports/persistence-port.d.ts +0 -4
  129. package/dist/app/ports/secret-port.d.ts +0 -5
  130. package/dist/app/ports/terminal-port.d.ts +0 -6
  131. package/dist/app/ports/updates-port.d.ts +0 -4
  132. package/dist/attachments/service.d.ts +29 -0
  133. package/dist/attachments/service.js +47 -0
  134. package/dist/attachments/service.js.map +1 -0
  135. package/dist/commands/providers.js +0 -3
  136. package/dist/commands/providers.js.map +1 -1
  137. package/dist/commands/search-providers.d.ts +0 -33
  138. package/dist/commands/search-providers.js +0 -33
  139. package/dist/commands/search-providers.js.map +1 -1
  140. package/dist/commands/update.js +1 -9
  141. package/dist/commands/update.js.map +1 -1
  142. package/dist/index.js +22 -42
  143. package/dist/index.js.map +1 -1
  144. package/dist/llm/adapters/ollama-tools.d.ts +0 -4
  145. package/dist/llm/adapters/ollama-tools.js +0 -4
  146. package/dist/llm/adapters/ollama-tools.js.map +1 -1
  147. package/dist/llm/capabilities.d.ts +10 -10
  148. package/dist/llm/capabilities.js +43 -17
  149. package/dist/llm/capabilities.js.map +1 -1
  150. package/dist/llm/groq.js +0 -6
  151. package/dist/llm/groq.js.map +1 -1
  152. package/dist/llm/http.d.ts +0 -17
  153. package/dist/llm/http.js +0 -45
  154. package/dist/llm/http.js.map +1 -1
  155. package/dist/llm/router.d.ts +0 -6
  156. package/dist/llm/router.js +0 -15
  157. package/dist/llm/router.js.map +1 -1
  158. package/dist/llm/tool-protocol.d.ts +0 -6
  159. package/dist/llm/tool-protocol.js +3 -7
  160. package/dist/llm/tool-protocol.js.map +1 -1
  161. package/dist/modes/agent.d.ts +2 -1
  162. package/dist/modes/agent.js +4 -2
  163. package/dist/modes/agent.js.map +1 -1
  164. package/dist/modes/ask.d.ts +0 -18
  165. package/dist/modes/ask.js +0 -49
  166. package/dist/modes/ask.js.map +1 -1
  167. package/dist/os/cwd.d.ts +0 -21
  168. package/dist/os/cwd.js +0 -24
  169. package/dist/os/cwd.js.map +1 -1
  170. package/dist/os/pkgmgr.js +0 -6
  171. package/dist/os/pkgmgr.js.map +1 -1
  172. package/dist/prompts/index.d.ts +6 -18
  173. package/dist/prompts/index.js +67 -293
  174. package/dist/prompts/index.js.map +1 -1
  175. package/dist/prompts/system.agent.md +242 -0
  176. package/dist/prompts/system.ask.md +56 -0
  177. package/dist/repl/prompt-line.js +0 -36
  178. package/dist/repl/prompt-line.js.map +1 -1
  179. package/dist/repl/slash-commands.d.ts +0 -9
  180. package/dist/repl/slash-commands.js +2 -23
  181. package/dist/repl/slash-commands.js.map +1 -1
  182. package/dist/repl.d.ts +5 -0
  183. package/dist/repl.js +169 -55
  184. package/dist/repl.js.map +1 -1
  185. package/dist/safety/classifier.js +17 -9
  186. package/dist/safety/classifier.js.map +1 -1
  187. package/dist/safety/engagement-policy.d.ts +46 -0
  188. package/dist/safety/engagement-policy.js +198 -0
  189. package/dist/safety/engagement-policy.js.map +1 -0
  190. package/dist/store/engagement.d.ts +115 -0
  191. package/dist/store/engagement.js +278 -0
  192. package/dist/store/engagement.js.map +1 -0
  193. package/dist/store/logs.d.ts +5 -0
  194. package/dist/store/logs.js +52 -2
  195. package/dist/store/logs.js.map +1 -1
  196. package/dist/store/plan.d.ts +57 -5
  197. package/dist/store/plan.js +182 -8
  198. package/dist/store/plan.js.map +1 -1
  199. package/dist/store/scope.d.ts +4 -1
  200. package/dist/store/scope.js +2 -2
  201. package/dist/store/scope.js.map +1 -1
  202. package/dist/tools/definitions.js +70 -1
  203. package/dist/tools/definitions.js.map +1 -1
  204. package/dist/tools/elevated-shell.d.ts +60 -0
  205. package/dist/tools/elevated-shell.js +233 -0
  206. package/dist/tools/elevated-shell.js.map +1 -0
  207. package/dist/tools/file-diff.d.ts +125 -0
  208. package/dist/tools/file-diff.js +495 -0
  209. package/dist/tools/file-diff.js.map +1 -0
  210. package/dist/tools/fs.d.ts +11 -13
  211. package/dist/tools/fs.js +269 -78
  212. package/dist/tools/fs.js.map +1 -1
  213. package/dist/tools/http.d.ts +15 -0
  214. package/dist/tools/http.js +202 -61
  215. package/dist/tools/http.js.map +1 -1
  216. package/dist/tools/jobs.d.ts +71 -10
  217. package/dist/tools/jobs.js +403 -124
  218. package/dist/tools/jobs.js.map +1 -1
  219. package/dist/tools/nmap-runner.d.ts +11 -0
  220. package/dist/tools/nmap-runner.js +115 -74
  221. package/dist/tools/nmap-runner.js.map +1 -1
  222. package/dist/tools/pentest-workflows.d.ts +8 -0
  223. package/dist/tools/pentest-workflows.js +71 -0
  224. package/dist/tools/pentest-workflows.js.map +1 -0
  225. package/dist/tools/registry.d.ts +9 -0
  226. package/dist/tools/registry.js +227 -56
  227. package/dist/tools/registry.js.map +1 -1
  228. package/dist/tools/shell.d.ts +3 -0
  229. package/dist/tools/shell.js +35 -13
  230. package/dist/tools/shell.js.map +1 -1
  231. package/dist/tools/tool-types.d.ts +11 -0
  232. package/dist/tui-v2/app/App.js +25 -39
  233. package/dist/tui-v2/app/App.js.map +1 -1
  234. package/dist/tui-v2/app/command-handlers.js +20 -1
  235. package/dist/tui-v2/app/command-handlers.js.map +1 -1
  236. package/dist/tui-v2/app/commands/picker-commands.js +3 -0
  237. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  238. package/dist/tui-v2/app/commands/session-commands.js +5 -0
  239. package/dist/tui-v2/app/commands/session-commands.js.map +1 -1
  240. package/dist/tui-v2/app/plan-lifecycle.d.ts +25 -3
  241. package/dist/tui-v2/app/plan-lifecycle.js +87 -24
  242. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
  243. package/dist/tui-v2/bootstrap/patch-opentui-text.d.ts +6 -0
  244. package/dist/tui-v2/bootstrap/patch-opentui-text.js +53 -0
  245. package/dist/tui-v2/bootstrap/patch-opentui-text.js.map +1 -0
  246. package/dist/tui-v2/bootstrap/start-tui-v2.js +7 -0
  247. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  248. package/dist/tui-v2/components/jobs/jobs-panel.js +23 -14
  249. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  250. package/dist/tui-v2/components/modal/confirm-modal.d.ts +1 -1
  251. package/dist/tui-v2/components/modal/confirm-modal.js +18 -7
  252. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
  253. package/dist/tui-v2/components/overlay/overlay-host.js +1 -1
  254. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  255. package/dist/tui-v2/components/pager/pager.d.ts +6 -0
  256. package/dist/tui-v2/components/pager/pager.js +313 -67
  257. package/dist/tui-v2/components/pager/pager.js.map +1 -1
  258. package/dist/tui-v2/components/status/status-line.d.ts +9 -0
  259. package/dist/tui-v2/components/status/status-line.js +17 -3
  260. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  261. package/dist/tui-v2/components/transcript/assistant-message.js +1 -1
  262. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  263. package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
  264. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  265. package/dist/tui-v2/components/transcript/tool-card.d.ts +9 -1
  266. package/dist/tui-v2/components/transcript/tool-card.js +114 -9
  267. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  268. package/dist/tui-v2/components/transcript/transcript-row.js +2 -2
  269. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  270. package/dist/tui-v2/composer/composer-editor.js +16 -0
  271. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  272. package/dist/tui-v2/controllers/overlay-controller.d.ts +16 -2
  273. package/dist/tui-v2/controllers/overlay-controller.js +92 -12
  274. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
  275. package/dist/tui-v2/rendering/artifact-pager-source.d.ts +19 -0
  276. package/dist/tui-v2/rendering/artifact-pager-source.js +103 -0
  277. package/dist/tui-v2/rendering/artifact-pager-source.js.map +1 -0
  278. package/dist/tui-v2/rendering/file-diff-view.d.ts +63 -0
  279. package/dist/tui-v2/rendering/file-diff-view.js +263 -0
  280. package/dist/tui-v2/rendering/file-diff-view.js.map +1 -0
  281. package/dist/tui-v2/rendering/open-tool-output.d.ts +9 -1
  282. package/dist/tui-v2/rendering/open-tool-output.js +65 -12
  283. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
  284. package/dist/tui-v2/rendering/pager-chrome.d.ts +12 -0
  285. package/dist/tui-v2/rendering/pager-chrome.js +56 -0
  286. package/dist/tui-v2/rendering/pager-chrome.js.map +1 -0
  287. package/dist/tui-v2/rendering/plan-view.js +1 -1
  288. package/dist/tui-v2/rendering/plan-view.js.map +1 -1
  289. package/dist/tui-v2/rendering/syntax-highlight.d.ts +37 -0
  290. package/dist/tui-v2/rendering/syntax-highlight.js +1303 -0
  291. package/dist/tui-v2/rendering/syntax-highlight.js.map +1 -0
  292. package/dist/tui-v2/rendering/theme.d.ts +28 -0
  293. package/dist/tui-v2/rendering/theme.js +28 -0
  294. package/dist/tui-v2/rendering/theme.js.map +1 -1
  295. package/dist/tui-v2/rendering/tool-presenter.d.ts +5 -0
  296. package/dist/tui-v2/rendering/tool-presenter.js +35 -3
  297. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
  298. package/dist/tui-v2/state/transcript-hydrate.js +5 -0
  299. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  300. package/dist/tui-v2/state/transcript-reducer.js +12 -2
  301. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  302. package/dist/tui-v2/state/transcript-store.d.ts +7 -0
  303. package/dist/tui-v2/state/transcript-store.js +46 -0
  304. package/dist/tui-v2/state/transcript-store.js.map +1 -1
  305. package/dist/tui-v2/state/transcript-types.d.ts +12 -0
  306. package/dist/tui-v2/state/transcript-types.js +9 -0
  307. package/dist/tui-v2/state/transcript-types.js.map +1 -1
  308. package/dist/types.d.ts +17 -1
  309. package/dist/ui/mentions.d.ts +6 -0
  310. package/dist/ui/mentions.js +50 -12
  311. package/dist/ui/mentions.js.map +1 -1
  312. package/dist/ui/plan-pane.js +1 -1
  313. package/dist/ui/plan-pane.js.map +1 -1
  314. package/package.json +16 -11
@@ -1,7 +1,9 @@
1
1
  import chalk from "chalk";
2
- import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
2
+ import { createPlan, loadPlan, savePlan, markTask, isBareTaskIdTitle, isPlanTerminal, isPlanSuccessful, readyPlanTasks, validateSessionPlan, } from "../store/plan.js";
3
3
  import { renderPlanChecklist } from "../ui/plan-pane.js";
4
4
  import { isLumpedSingleTask } from "./tool-call-parser.js";
5
+ import { getActiveProjectRoot } from "./project-root.js";
6
+ import { detectPackageManager } from "./workspace-orient.js";
5
7
  /** Titles match for plan merge (exact or mutual long substring). */
6
8
  export function titlesMatchForPlan(a, b) {
7
9
  const t1 = a.trim().toLowerCase();
@@ -71,6 +73,67 @@ export function ensureCodingPlanInstallTask(kind, goal, detail, tasks) {
71
73
  out.splice(insertAt, 0, "Install project dependencies (npm/yarn/pnpm/bun install in project root)");
72
74
  return out;
73
75
  }
76
+ function looksLikeFeatureImplementTask(title) {
77
+ const t = title.toLowerCase();
78
+ if (looksLikeInstallTask(title) || looksLikeRunServerTask(title))
79
+ return false;
80
+ if (looksLikeScaffoldishTask(title) && !/\b(feature|todo|component|ui)\b/.test(t)) {
81
+ return false;
82
+ }
83
+ return (/\b(implement|feature|todo|component|ui|crud|auth|localstorage|persist|integrate)\b/.test(t) ||
84
+ (/\b(add|build|write|create)\b/.test(t) &&
85
+ /\b(todo|feature|component|page|ui|list|form)\b/.test(t)));
86
+ }
87
+ function looksLikeFeatureAppPlan(goal, detail, tasks) {
88
+ const blob = `${goal}\n${detail}\n${tasks.join("\n")}`.toLowerCase();
89
+ if (/\b(just|only)\s+(scaffold|init|boilerplate|starter)\b/.test(blob)) {
90
+ return false;
91
+ }
92
+ return (/\b(todo|to-?do|blog|dashboard|chat|kanban|notes?|crm|shop|auth|login|crud|portfolio)\b/.test(blob) &&
93
+ /\b(app|application|project|site|ui|feature)\b/.test(blob));
94
+ }
95
+ /**
96
+ * Ensure feature-app plans include an implement-feature task (not scaffold-only).
97
+ */
98
+ export function ensureCodingPlanFeatureTask(kind, goal, detail, tasks) {
99
+ if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
100
+ return tasks;
101
+ if (!looksLikeFeatureAppPlan(goal, detail, tasks))
102
+ return tasks;
103
+ if (tasks.some(looksLikeFeatureImplementTask))
104
+ return tasks;
105
+ const out = [...tasks];
106
+ // Insert after install if present, else after scaffold, else before last (verify)
107
+ const installIdx = out.findIndex(looksLikeInstallTask);
108
+ const scaffoldIdx = out.findIndex(looksLikeScaffoldishTask);
109
+ const runIdx = out.findIndex(looksLikeRunServerTask);
110
+ let insertAt;
111
+ if (installIdx >= 0)
112
+ insertAt = installIdx + 1;
113
+ else if (scaffoldIdx >= 0)
114
+ insertAt = scaffoldIdx + 1;
115
+ else if (runIdx >= 0)
116
+ insertAt = runIdx;
117
+ else
118
+ insertAt = out.length;
119
+ out.splice(insertAt, 0, "Implement the requested product feature (replace starter boilerplate with real UI/state)");
120
+ return out;
121
+ }
122
+ /** Ensure local app plans end with shell.start + probe + leave running. */
123
+ export function ensureCodingPlanRunVerifyTask(kind, goal, detail, tasks) {
124
+ if (!looksLikeLocalAppScaffold(kind, goal, detail, tasks))
125
+ return tasks;
126
+ if (tasks.some(looksLikeRunServerTask))
127
+ return tasks;
128
+ return [
129
+ ...tasks,
130
+ "Start dev server with shell.start, probe localhost, leave running, report URL/port/job id",
131
+ ];
132
+ }
133
+ /** Preserve the model/user-authored checklist exactly for fresh coding plans. */
134
+ export function normalizeCodingPlanTasks(_kind, _goal, _detail, tasks) {
135
+ return [...tasks];
136
+ }
74
137
  /** Goal is only "run/start the existing app" — do not open a new plan. */
75
138
  export function looksLikeRunOnlyGoal(goal, detail) {
76
139
  const blob = `${goal} ${detail}`.toLowerCase();
@@ -173,14 +236,20 @@ export function normalizePlanTaskEntries(args) {
173
236
  .split(/\n|;|(?:,\s*(?=[A-Z0-9\-]))/)
174
237
  .map((s) => s.replace(/^\s*[-*\d.)]+\s*/, "").trim())
175
238
  .filter(Boolean)
176
- .map((title) => ({ title, aliases: [] }));
239
+ .map((title) => ({
240
+ title,
241
+ aliases: [],
242
+ dependencies: [],
243
+ dependenciesSpecified: false,
244
+ resourceLocks: [],
245
+ }));
177
246
  }
178
247
  if (!Array.isArray(raw))
179
248
  return [];
180
249
  return raw
181
250
  .map((t) => {
182
251
  const title = normalizeTaskTitle(t);
183
- if (!title)
252
+ if (!title || isBareTaskIdTitle(title))
184
253
  return null;
185
254
  const aliases = [];
186
255
  if (t && typeof t === "object") {
@@ -200,7 +269,18 @@ export function normalizePlanTaskEntries(args) {
200
269
  const slug = slugifyTaskId(title);
201
270
  if (slug && !aliases.includes(slug))
202
271
  aliases.push(slug);
203
- return { title, aliases: [...new Set(aliases)] };
272
+ const object = t && typeof t === "object" ? t : {};
273
+ const stringArray = (value) => Array.isArray(value)
274
+ ? [...new Set(value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean))]
275
+ : [];
276
+ return {
277
+ title,
278
+ aliases: [...new Set(aliases)],
279
+ dependencies: stringArray(object.dependencies ?? object.dependsOn),
280
+ dependenciesSpecified: Object.prototype.hasOwnProperty.call(object, "dependencies") ||
281
+ Object.prototype.hasOwnProperty.call(object, "dependsOn"),
282
+ resourceLocks: stringArray(object.resourceLocks ?? object.resources),
283
+ };
204
284
  })
205
285
  .filter((x) => Boolean(x));
206
286
  }
@@ -217,13 +297,6 @@ export function resolvePlanTaskId(plan, taskId) {
217
297
  if (t.aliases?.some((a) => a === raw || a.toLowerCase() === lower)) {
218
298
  return t.id;
219
299
  }
220
- if (slugifyTaskId(t.title) === slug)
221
- return t.id;
222
- if (slug.length > 4 &&
223
- (slugifyTaskId(t.title).includes(slug) ||
224
- slug.includes(slugifyTaskId(t.title)))) {
225
- return t.id;
226
- }
227
300
  }
228
301
  return undefined;
229
302
  }
@@ -234,7 +307,7 @@ export function renderPlanForTerminal(plan) {
234
307
  /** Build the system-context block describing the session's active plan. */
235
308
  export function planContextMessage(plan, approved) {
236
309
  const lines = [];
237
- lines.push(`ACTIVE PLAN for this session (goal: ${plan.goal}, status: ${plan.status}):`);
310
+ lines.push(`ACTIVE PLAN v${plan.version ?? 1} for this session (goal: ${plan.goal}, status: ${plan.status}):`);
238
311
  if (plan.detail.trim())
239
312
  lines.push(plan.detail.trim());
240
313
  lines.push("Tasks:");
@@ -242,39 +315,49 @@ export function planContextMessage(plan, approved) {
242
315
  const aliasHint = t.aliases?.length && t.aliases[0] !== t.id
243
316
  ? ` [aliases: ${t.aliases.slice(0, 3).join(", ")}]`
244
317
  : "";
245
- lines.push(` ${i + 1}. [${t.id}] (${t.state}) ${t.title}${aliasHint}`);
318
+ const dependencyHint = t.dependencies?.length
319
+ ? ` [depends: ${t.dependencies.join(", ")}]`
320
+ : "";
321
+ const resourceHint = t.resourceLocks?.length
322
+ ? ` [locks: ${t.resourceLocks.join(", ")}]`
323
+ : "";
324
+ const evidenceHint = t.evidence?.successWorkCount
325
+ ? ` [evidence: ${t.evidence.successWorkCount} successful tool${t.evidence.successWorkCount === 1 ? "" : "s"}${t.evidence.lastOkTool ? `; last ${t.evidence.lastOkTool}` : ""}]`
326
+ : "";
327
+ lines.push(` ${i + 1}. [${t.id}] (${t.state}) ${t.title}${aliasHint}${dependencyHint}${resourceHint}${evidenceHint}`);
246
328
  });
247
329
  lines.push("task.update taskId MUST be t1, t2, … from this list (or a listed alias) — never invent free-form slugs alone.");
330
+ if (plan.meta?.projectRoot) {
331
+ lines.push(`project_root: ${plan.meta.projectRoot}`);
332
+ }
333
+ if (plan.meta?.packageManager) {
334
+ lines.push(`package_manager: ${plan.meta.packageManager}`);
335
+ }
248
336
  if (approved) {
249
337
  const inProgress = plan.tasks.find((t) => t.state === "in_progress");
250
- const firstPending = plan.tasks.find((t) => t.state === "pending");
251
- lines.push("The user APPROVED this plan. Execute it task by task NOW.");
338
+ const firstPending = readyPlanTasks(plan)[0];
339
+ lines.push("The user APPROVED this plan. Execute it NOW. Tasks are checkpoints — you still own the whole goal.");
252
340
  if (inProgress) {
253
341
  lines.push(`RESUME TASK ${inProgress.id} (${inProgress.title}) — it was started but interrupted. ` +
254
342
  "Retry what was in progress; do NOT restart completed work from scratch. " +
255
- "Do NOT re-do tasks already marked done, and do NOT skip ahead to later tasks.");
343
+ "Do NOT re-do tasks already marked done.");
256
344
  }
257
345
  else if (firstPending) {
258
346
  lines.push(`START WITH TASK ${firstPending.id} (${firstPending.title}). ` +
259
- "Do NOT re-do tasks already marked done, and do NOT skip ahead to later tasks.");
347
+ "Do NOT re-do tasks already marked done.");
260
348
  }
261
- lines.push("STRICT ORDER: call task.update {taskId, state:'in_progress'}do the real work → " +
262
- "WAIT for and READ the tool result only if you are satisfied it succeeded, " +
263
- "call task.update {taskId, state:'done'} then open the NEXT task. " +
264
- "Never mark done before a successful tool result. Never start the next task's work " +
265
- "(or mark a later task in_progress) until the current one is done and verified. " +
266
- "If a tool fails, mark the task 'failed' with a note, fix, retry. " +
267
- "Never re-run tasks already marked done. " +
268
- "For run/verify: shell.start, leave server running, final message includes URL, port, and job id.");
349
+ lines.push("Flow: task.update in_progress → real work → WAIT for and READ the tool result → " +
350
+ "mark done only when verified next task. Durable evidence shown beside a task survives resume; use it to close that task rather than repeating already-confirmed work. " +
351
+ "Independent read-only lookups may parallelize within a task. " +
352
+ "Never mark done before successful relevant evidence. If a tool fails: mark failed, fix, retry. " +
353
+ "For run/verify: prove runtime (shell.start, ready tail, LISTEN, or localhost GET), leave server running, final message includes URL, port, and job id. " +
354
+ "Do not re-open done tasks. If mid-task evidence shows the plan is wrong, adapt (fix root cause; for pentest, revise plan with completed tasks preserved).");
269
355
  }
270
356
  else {
271
- lines.push("This plan is NOT yet approved, so you MUST NOT execute any of its tasks yet. " +
272
- "Any new free-text message from the user right now is a PLAN REVISION, not approval — even if it " +
273
- "sounds like an instruction (e.g. 'do not install new tools', 'use only X', 'also add Y', 'skip task 2'). " +
274
- "Treat it as feedback: call plan.create AGAIN with the revised goal/detail/tasks to produce an updated " +
275
- "plan, then STOP and wait. Do NOT call shell.exec, pkg.install, net.scan, tool.check, fs.write, or any " +
276
- "other execution tool. The user will APPROVE with /implement, or CANCEL with /discard. Only after " +
277
- "/implement may you begin executing.");
357
+ lines.push("This plan is NOT yet approved do not execute tasks (no scaffold/write/install/exploit). " +
358
+ "User free-text is PLAN REVISION feedback, not approval — even if it sounds like an instruction. " +
359
+ "Refine with plan.create and/or read-only explore/research, then STOP. " +
360
+ "User may Accept, Discard, View, or Suggest changes (or /implement / /discard).");
278
361
  }
279
362
  return lines.join("\n");
280
363
  }
@@ -284,7 +367,9 @@ export function planContextMessage(plan, approved) {
284
367
  * the agent keeps it in context across the whole session.
285
368
  */
286
369
  export async function handlePlanTool(call, session, ctx) {
287
- void ctx;
370
+ const autoApprove = Boolean(ctx.autoApprove);
371
+ void ctx.loopGuard;
372
+ void ctx.step;
288
373
  if (call.name === "plan.create") {
289
374
  const goal = normalizePlanGoal(call.args);
290
375
  const detail = normalizePlanDetail(call.args);
@@ -310,10 +395,9 @@ export async function handlePlanTool(call, session, ctx) {
310
395
  handled: true,
311
396
  ok: false,
312
397
  display: chalk.red(" ✗ plan.create: that single task lumps the whole build into one step\n"),
313
- modelNote: "plan.create rejected: you put everything into ONE task. Break it into 3-8 SEPARATE, " +
314
- "ordered tasks — each a distinct action, e.g. 'scaffold package.json + vite config', " +
315
- "'create index.html + entry (main.jsx)', 'build App + Post components', 'add posts data + styles', " +
316
- "'install deps and run dev server to verify'. Call plan.create again with that tasks array.",
398
+ modelNote: "plan.create rejected: you put everything into ONE task. Break it into separate " +
399
+ "ordered tasks — each a distinct action (scaffold, implement feature, install, run/verify). " +
400
+ "Call plan.create again with a proper tasks array.",
317
401
  };
318
402
  }
319
403
  const existingPlan = await loadPlan(session.sessionId).catch(() => undefined);
@@ -333,40 +417,48 @@ export async function handlePlanTool(call, session, ctx) {
333
417
  "the URL (http://localhost:<port>), port, and job id.",
334
418
  };
335
419
  }
336
- // X12: coding/local-app plans must include a final run/verify server task.
337
- if (codingPlanNeedsRunVerifyTask(kind, goal, detail, taskTitles)) {
338
- return {
339
- handled: true,
340
- ok: false,
341
- display: chalk.red(" ✗ plan.create: coding app plan needs a final run/verify server task\n"),
342
- modelNote: "plan.create rejected: for local web/app scaffolds (React/Vite/Next/etc.), tasks MUST end with a " +
343
- "final run/verify step — e.g. 'Start dev server (shell.start), tail until ready, probe localhost, " +
344
- "leave server running, report URL/port/job id'. Prefer shell.start (background); do not shell.stop " +
345
- "unless the user asks. Build alone is not enough. Add that final task and call plan.create again. " +
346
- "Pure libraries/CLIs with no server do not need this.",
347
- };
348
- }
349
- // Inject install step on fresh plans when model forgot it (avoids install
350
- // blocked under implement + wrong auto-start attribution). Skip rewrite on
351
- // merge/revise so completed task order stays stable.
420
+ // Preserve authored task count/order. Build, install, and verification
421
+ // evidence may span tasks; the runtime must not inject regex-derived
422
+ // checkpoints that change IDs or make progress diverge from the plan.
352
423
  const normalizedTitles = existingPlan
353
424
  ? taskTitles
354
- : ensureCodingPlanInstallTask(kind, goal, detail, taskTitles);
425
+ : normalizeCodingPlanTasks(kind, goal, detail, taskTitles);
426
+ const root = getActiveProjectRoot();
427
+ const pm = root ? detectPackageManager(root) : undefined;
428
+ const meta = root || existingPlan?.meta
429
+ ? {
430
+ ...(existingPlan?.meta ?? {}),
431
+ ...(root ? { projectRoot: root } : {}),
432
+ ...(pm ? { packageManager: pm } : {}),
433
+ }
434
+ : undefined;
355
435
  const plan = createPlan({
356
436
  sessionId: session.sessionId,
357
437
  goal,
358
438
  detail,
359
439
  taskTitles: normalizedTitles,
360
440
  kind,
441
+ meta,
361
442
  });
362
443
  // Attach model-supplied slug aliases so task.update can resolve them (X3).
363
444
  for (let i = 0; i < plan.tasks.length; i++) {
364
445
  const aliases = taskEntries[i]?.aliases ?? [];
365
446
  if (aliases.length)
366
447
  plan.tasks[i].aliases = aliases;
448
+ const locks = taskEntries[i]?.resourceLocks ?? [];
449
+ if (locks.length)
450
+ plan.tasks[i].resourceLocks = locks;
451
+ }
452
+ for (let i = 0; i < plan.tasks.length; i++) {
453
+ const rawDependencies = taskEntries[i]?.dependencies ?? [];
454
+ const dependencies = rawDependencies.map((dependency) => resolvePlanTaskId(plan, dependency) ?? dependency);
455
+ if (taskEntries[i]?.dependenciesSpecified) {
456
+ plan.tasks[i].dependencies = [...new Set(dependencies)];
457
+ }
367
458
  }
368
459
  let additiveOnly = false;
369
460
  if (existingPlan) {
461
+ plan.version = (existingPlan.version ?? 1) + 1;
370
462
  const usedOldIds = new Set();
371
463
  const mappedNewTasks = plan.tasks.map((task) => {
372
464
  const match = existingPlan.tasks.find((t) => !usedOldIds.has(t.id) && titlesMatchForPlan(t.title, task.title));
@@ -377,6 +469,7 @@ export async function handlePlanTool(call, session, ctx) {
377
469
  id: match.id,
378
470
  state: match.state,
379
471
  note: match.note,
472
+ evidence: match.evidence,
380
473
  };
381
474
  }
382
475
  return task;
@@ -398,8 +491,32 @@ export async function handlePlanTool(call, session, ctx) {
398
491
  taken.add(task.id);
399
492
  }
400
493
  }
401
- const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => !mappedNewTasks.some((newTask) => titlesMatchForPlan(oldTask.title, newTask.title)));
402
- plan.tasks = [...oldTasksToKeep, ...mappedNewTasks];
494
+ const oldTasksToKeep = existingPlan.tasks.filter((oldTask) => !isBareTaskIdTitle(oldTask.title) &&
495
+ !mappedNewTasks.some((newTask) => titlesMatchForPlan(oldTask.title, newTask.title)));
496
+ plan.tasks = [...oldTasksToKeep, ...mappedNewTasks].filter((t) => !isBareTaskIdTitle(t.title));
497
+ const finalIdByInputReference = new Map();
498
+ for (let index = 0; index < mappedNewTasks.length; index += 1) {
499
+ const task = mappedNewTasks[index];
500
+ const entry = taskEntries[index];
501
+ finalIdByInputReference.set(`t${index + 1}`, task.id);
502
+ finalIdByInputReference.set(task.id, task.id);
503
+ finalIdByInputReference.set(slugifyTaskId(task.title), task.id);
504
+ for (const alias of entry?.aliases ?? []) {
505
+ finalIdByInputReference.set(alias, task.id);
506
+ }
507
+ }
508
+ for (let index = 0; index < mappedNewTasks.length; index += 1) {
509
+ const task = mappedNewTasks[index];
510
+ const entry = taskEntries[index];
511
+ const references = entry?.dependenciesSpecified
512
+ ? entry.dependencies
513
+ : index > 0
514
+ ? [`t${index}`]
515
+ : [];
516
+ task.dependencies = [...new Set(references.map((reference) => finalIdByInputReference.get(reference) ??
517
+ resolvePlanTaskId(plan, reference) ??
518
+ reference))];
519
+ }
403
520
  // X7: keep approval when only additive pending work remains.
404
521
  const priorFinished = existingPlan.tasks.filter((t) => t.state === "done" || t.state === "skipped" || t.state === "failed");
405
522
  const finishedStillFinished = priorFinished.length > 0 &&
@@ -427,13 +544,27 @@ export async function handlePlanTool(call, session, ctx) {
427
544
  plan.createdAt = existingPlan.createdAt;
428
545
  }
429
546
  }
430
- await savePlan(plan).catch(() => undefined);
547
+ const dag = validateSessionPlan(plan);
548
+ if (!dag.ok) {
549
+ return {
550
+ handled: true,
551
+ ok: false,
552
+ display: chalk.red(` ✗ plan.create: invalid dependency graph — ${dag.reason}\n`),
553
+ modelNote: `plan.create failed: ${dag.reason}. Use only declared task ids/aliases and remove dependency cycles.`,
554
+ };
555
+ }
431
556
  if (additiveOnly) {
432
557
  session.planApproved.value = true;
558
+ plan.status = "in_progress";
559
+ }
560
+ else if (autoApprove) {
561
+ session.planApproved.value = true;
562
+ plan.status = "approved";
433
563
  }
434
564
  else {
435
565
  session.planApproved.value = false;
436
566
  }
567
+ await savePlan(plan).catch(() => undefined);
437
568
  const checklist = renderPlanForTerminal(plan);
438
569
  const display = additiveOnly
439
570
  ? chalk.cyan(" ● plan updated (additive)\n") +
@@ -441,12 +572,17 @@ export async function handlePlanTool(call, session, ctx) {
441
572
  "\n" +
442
573
  chalk.dim(" ✦ done tasks preserved — continue from the first pending task only.\n" +
443
574
  " Do NOT re-run completed scaffold/build work.\n")
444
- : chalk.cyan(" ● planning\n") +
445
- checklist +
446
- "\n" +
447
- chalk.dim(" ✦ plan created — press Ctrl+P to view it, /implement to approve and run it,\n" +
448
- " or /discard to cancel it. Any other message refines this plan.\n");
449
- const firstPending = plan.tasks.find((t) => t.state === "pending");
575
+ : autoApprove
576
+ ? chalk.cyan(" ● plan created (auto-approved)\n") +
577
+ checklist +
578
+ "\n" +
579
+ chalk.dim(" plan approved in agent mode continue executing task by task.\n")
580
+ : chalk.cyan(" ● planning\n") +
581
+ checklist +
582
+ "\n" +
583
+ chalk.dim(" ✦ plan created — Accept to run, Discard, View, or Suggest changes\n" +
584
+ " (/implement, /discard, Ctrl+P also work).\n");
585
+ const firstPending = readyPlanTasks(plan)[0];
450
586
  return {
451
587
  handled: true,
452
588
  ok: true,
@@ -461,15 +597,17 @@ export async function handlePlanTool(call, session, ctx) {
461
597
  "If the only new work is run/verify on an existing app, prefer shell.start + probe + report URL — " +
462
598
  "do not reboot the world. When the run/verify task finishes, final message MUST include " +
463
599
  "http://localhost:<port>, port, job id, and that the server is still running."
464
- : `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
465
- "Do NOT start executing tasks until the user approves with /implement. " +
466
- "If the user's next message gives feedback instead of /implement, that is a REVISION: call plan.create " +
467
- "again with the updated plan and STOP again. The user may cancel the whole plan with /discard. " +
468
- "Only after /implement do you begin, working task by task, calling task.update to mark each " +
469
- "in_progress before and done after you finish it. " +
470
- "Do NOT call plan.create again only to add a run-dev-server step put that in this plan's final task, " +
471
- "or after completion use shell.start + probe directly. " +
472
- "When run/verify completes: report URL, port, job id, and that the server is still running.",
600
+ : autoApprove
601
+ ? `Plan saved and approved with ${plan.tasks.length} task(s). Execute now. ` +
602
+ (firstPending
603
+ ? `Start with [${firstPending.id}] "${firstPending.title}". `
604
+ : "") +
605
+ "task.update in_progress work → verify → done. When run/verify completes: report URL, port, job id, server still running."
606
+ : `Plan saved with ${plan.tasks.length} task(s). STOP here and waitproduce NO other tool calls now. ` +
607
+ "Do NOT start executing until the user accepts the plan. " +
608
+ "If the user's next message gives feedback, that is a REVISION: call plan.create again with the updated plan and STOP again. " +
609
+ "The user may discard the plan. After acceptance: task.update in_progress → work → verify → done. " +
610
+ "When run/verify completes: report URL, port, job id, and that the server is still running.",
473
611
  };
474
612
  }
475
613
  // task.update
@@ -506,43 +644,103 @@ export async function handlePlanTool(call, session, ctx) {
506
644
  }
507
645
  // X3: accept t1..tn or model slug aliases / title slugs.
508
646
  const taskId = resolvePlanTaskId(plan, taskIdRaw) ?? taskIdRaw;
509
- // Only one task may be in_progress at a time. This forces genuine
510
- // task-by-task execution: the model must close (done/failed/skipped) the
511
- // current task before opening the next one, instead of leaving a task
512
- // "in_progress" as an umbrella while it quietly works through the rest
513
- // of the plan underneath it.
514
647
  if (stateRaw === "in_progress") {
515
- const otherInProgress = plan.tasks.find((t) => t.id !== taskId && t.state === "in_progress");
516
- if (otherInProgress) {
648
+ const target = plan.tasks.find((task) => task.id === taskId);
649
+ const ready = readyPlanTasks(plan).some((task) => task.id === taskId);
650
+ const retryingFailedTask = target?.state === "failed";
651
+ if (target?.state === "done" || target?.state === "skipped") {
652
+ const nextPending = readyPlanTasks(plan)[0];
653
+ const nextHint = nextPending
654
+ ? ` Continue with task.update {taskId:"${nextPending.id}", state:"in_progress"} ("${nextPending.title}").`
655
+ : " All ready work is finished — write the final summary if needed.";
517
656
  return {
518
657
  handled: true,
519
658
  ok: false,
520
- display: chalk.red(` \u2717 task.update: task [${otherInProgress.id}] "${otherInProgress.title}" is still in_progress\n`),
521
- modelNote: `task.update failed: task [${otherInProgress.id}] "${otherInProgress.title}" is still in_progress. ` +
522
- "Finish it first \u2014 call task.update with state 'done' (or 'failed'/'skipped' with a note) " +
523
- `for [${otherInProgress.id}] before starting [${taskId}]. ` +
524
- "Use ONLY ids like t1, t2 from the plan context (not title slugs).",
659
+ display: chalk.red(` task.update: [${taskId}] is already ${target.state} do not re-open\n`),
660
+ modelNote: `task.update failed: [${taskId}] is already ${target.state}. ` +
661
+ `Do not re-run completed tasks.${nextHint}`,
525
662
  };
526
663
  }
664
+ if (target?.state !== "in_progress") {
665
+ const incompleteDependencies = (target?.dependencies ?? []).filter((dependency) => {
666
+ const dependencyTask = plan.tasks.find((task) => task.id === dependency);
667
+ return !dependencyTask || (dependencyTask.state !== "done" && dependencyTask.state !== "skipped");
668
+ });
669
+ const heldLocks = new Set(plan.tasks
670
+ .filter((task) => task.state === "in_progress" && task.id !== taskId)
671
+ .flatMap((task) => task.resourceLocks ?? []));
672
+ const conflictingLocks = (target?.resourceLocks ?? []).filter((lock) => heldLocks.has(lock));
673
+ if ((!ready && !retryingFailedTask) ||
674
+ incompleteDependencies.length > 0 ||
675
+ conflictingLocks.length > 0) {
676
+ const nextReady = readyPlanTasks(plan)[0];
677
+ return {
678
+ handled: true,
679
+ ok: false,
680
+ display: chalk.red(` ✗ task.update: [${taskId}] is not dependency/resource ready\n`),
681
+ modelNote: `task.update failed: [${taskId}] is not ready. ` +
682
+ (incompleteDependencies.length ? `Incomplete dependencies: ${incompleteDependencies.join(", ")}. ` : "") +
683
+ (conflictingLocks.length ? `Resources currently locked: ${conflictingLocks.join(", ")}. ` : "") +
684
+ (nextReady
685
+ ? `Open the next ready task first: ${nextReady.id} ("${nextReady.title}").`
686
+ : ""),
687
+ };
688
+ }
689
+ }
690
+ // A retry is a new execution attempt. Previous receipts proved only the
691
+ // failed attempt and must not close the task before recovery work succeeds.
692
+ if (retryingFailedTask && target)
693
+ target.evidence = undefined;
527
694
  }
528
- // X8: cannot mark later task done while an earlier task is still open.
529
695
  if (stateRaw === "done") {
530
- const targetIdx = plan.tasks.findIndex((t) => t.id === taskId);
531
- if (targetIdx > 0) {
532
- const blocking = plan.tasks
533
- .slice(0, targetIdx)
534
- .filter((t) => t.state === "pending" ||
535
- t.state === "in_progress" ||
536
- t.state === "failed");
537
- if (blocking.length > 0) {
538
- const b = blocking[0];
696
+ const target = plan.tasks.find((task) => task.id === taskId);
697
+ const incompleteDependencies = (target?.dependencies ?? []).filter((dependency) => {
698
+ const dependencyTask = plan.tasks.find((task) => task.id === dependency);
699
+ return !dependencyTask || (dependencyTask.state !== "done" && dependencyTask.state !== "skipped");
700
+ });
701
+ if (incompleteDependencies.length > 0) {
702
+ return {
703
+ handled: true,
704
+ ok: false,
705
+ display: chalk.red(` ✗ task.update: cannot complete [${taskId}] before dependencies\n`),
706
+ modelNote: `task.update failed: earlier task/dependency ${incompleteDependencies.join(", ")} is not complete for [${taskId}].`,
707
+ };
708
+ }
709
+ if (target && target.state !== "in_progress") {
710
+ // Soft-auto: pending + dependency-ready → open then complete in one call.
711
+ // Models often skip the ritual in_progress step after work already landed.
712
+ if (target.state === "pending" && incompleteDependencies.length === 0) {
713
+ const opened = markTask(plan, taskId, "in_progress", note);
714
+ if (!opened) {
715
+ return {
716
+ handled: true,
717
+ ok: false,
718
+ display: chalk.red(` ✗ task.update: could not auto-open [${taskId}] before completion\n`),
719
+ modelNote: `task.update failed: could not auto-open pending [${taskId}].`,
720
+ };
721
+ }
722
+ // Fall through to mark done below (state is now in_progress).
723
+ }
724
+ else if (target.state === "failed") {
725
+ return {
726
+ handled: true,
727
+ ok: false,
728
+ display: chalk.red(` ✗ task.update: [${taskId}] is failed — retry with in_progress first\n`),
729
+ modelNote: `task.update failed: [${taskId}] is failed. ` +
730
+ `Call task.update {taskId:"${taskId}", state:"in_progress"} to retry, then mark done after recovery work.`,
731
+ };
732
+ }
733
+ else {
734
+ const nextReady = readyPlanTasks(plan)[0];
539
735
  return {
540
736
  handled: true,
541
737
  ok: false,
542
- display: chalk.red(` ✗ task.update: cannot mark [${taskId}] done while [${b.id}] is ${b.state}\n`),
543
- modelNote: `task.update failed: cannot mark [${taskId}] done while earlier task ` +
544
- `[${b.id}] "${b.title}" is still ${b.state}. ` +
545
- `Finish or skip [${b.id}] first (state done/skipped), then mark [${taskId}] done.`,
738
+ display: chalk.red(` ✗ task.update: [${taskId}] must be in_progress before completion\n`),
739
+ modelNote: `task.update failed: [${taskId}] is ${target.state}. ` +
740
+ "Start or retry it, perform fresh work, then mark it done." +
741
+ (nextReady
742
+ ? ` Open next ready: ${nextReady.id} ("${nextReady.title}").`
743
+ : ""),
546
744
  };
547
745
  }
548
746
  }
@@ -564,18 +762,25 @@ export async function handlePlanTool(call, session, ctx) {
564
762
  if (plan.status === "draft" || plan.status === "approved") {
565
763
  plan.status = "in_progress";
566
764
  }
567
- const allDone = plan.tasks.every((t) => t.state === "done" || t.state === "skipped" || t.state === "failed");
568
- if (allDone)
569
- plan.status = "completed";
765
+ const terminal = isPlanTerminal(plan);
766
+ const successful = isPlanSuccessful(plan);
767
+ if (terminal)
768
+ plan.status = successful ? "completed" : "abandoned";
570
769
  await savePlan(plan).catch(() => undefined);
571
770
  const checklist = renderPlanForTerminal(plan);
572
- const nextPending = plan.tasks.find((t) => t.state === "pending");
771
+ const nextPending = readyPlanTasks(plan)[0];
573
772
  let modelNote;
574
- if (allDone) {
773
+ if (successful) {
575
774
  modelNote =
576
- "Task updated. ALL tasks are now finished. Verify the result and give your final summary. " +
775
+ "Task updated. All required tasks succeeded or were explicitly skipped. Verify the result and give your final summary. " +
577
776
  "If a dev server was started: report URL, port, job id, and that it is still running.";
578
777
  }
778
+ else if (terminal) {
779
+ const failed = plan.tasks.filter((task) => task.state === "failed");
780
+ modelNote =
781
+ `Task updated. The plan is terminal but NOT successful: ${failed.length} task(s) failed. ` +
782
+ "Do not claim completion. Report a failed/partial outcome with the failed tasks and remaining user outcome.";
783
+ }
579
784
  else if (stateRaw === "done" && nextPending) {
580
785
  modelNote =
581
786
  `Task [${taskId}] marked done after verified work. ` +