@pentoshi/clai 3.6.0 → 3.7.3

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 (322) 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 +312 -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 +1226 -971
  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 +111 -29
  55. package/dist/agent/task-evidence.js +519 -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/embedded.d.ts +1 -0
  173. package/dist/prompts/embedded.js +9 -0
  174. package/dist/prompts/embedded.js.map +1 -0
  175. package/dist/prompts/index.d.ts +6 -18
  176. package/dist/prompts/index.js +104 -293
  177. package/dist/prompts/index.js.map +1 -1
  178. package/dist/prompts/system.agent.md +228 -0
  179. package/dist/prompts/system.ask.md +56 -0
  180. package/dist/repl/prompt-line.js +0 -36
  181. package/dist/repl/prompt-line.js.map +1 -1
  182. package/dist/repl/slash-commands.d.ts +0 -9
  183. package/dist/repl/slash-commands.js +2 -23
  184. package/dist/repl/slash-commands.js.map +1 -1
  185. package/dist/repl.d.ts +5 -0
  186. package/dist/repl.js +169 -55
  187. package/dist/repl.js.map +1 -1
  188. package/dist/safety/classifier.js +17 -9
  189. package/dist/safety/classifier.js.map +1 -1
  190. package/dist/safety/engagement-policy.d.ts +46 -0
  191. package/dist/safety/engagement-policy.js +198 -0
  192. package/dist/safety/engagement-policy.js.map +1 -0
  193. package/dist/store/engagement.d.ts +115 -0
  194. package/dist/store/engagement.js +278 -0
  195. package/dist/store/engagement.js.map +1 -0
  196. package/dist/store/logs.d.ts +5 -0
  197. package/dist/store/logs.js +52 -2
  198. package/dist/store/logs.js.map +1 -1
  199. package/dist/store/plan.d.ts +57 -5
  200. package/dist/store/plan.js +182 -8
  201. package/dist/store/plan.js.map +1 -1
  202. package/dist/store/scope.d.ts +4 -1
  203. package/dist/store/scope.js +2 -2
  204. package/dist/store/scope.js.map +1 -1
  205. package/dist/tools/definitions.js +70 -1
  206. package/dist/tools/definitions.js.map +1 -1
  207. package/dist/tools/elevated-shell.d.ts +60 -0
  208. package/dist/tools/elevated-shell.js +233 -0
  209. package/dist/tools/elevated-shell.js.map +1 -0
  210. package/dist/tools/file-diff.d.ts +125 -0
  211. package/dist/tools/file-diff.js +495 -0
  212. package/dist/tools/file-diff.js.map +1 -0
  213. package/dist/tools/fs.d.ts +11 -13
  214. package/dist/tools/fs.js +269 -78
  215. package/dist/tools/fs.js.map +1 -1
  216. package/dist/tools/http.d.ts +15 -0
  217. package/dist/tools/http.js +202 -61
  218. package/dist/tools/http.js.map +1 -1
  219. package/dist/tools/jobs.d.ts +71 -10
  220. package/dist/tools/jobs.js +403 -124
  221. package/dist/tools/jobs.js.map +1 -1
  222. package/dist/tools/nmap-runner.d.ts +11 -0
  223. package/dist/tools/nmap-runner.js +115 -74
  224. package/dist/tools/nmap-runner.js.map +1 -1
  225. package/dist/tools/pentest-workflows.d.ts +8 -0
  226. package/dist/tools/pentest-workflows.js +71 -0
  227. package/dist/tools/pentest-workflows.js.map +1 -0
  228. package/dist/tools/registry.d.ts +9 -0
  229. package/dist/tools/registry.js +244 -57
  230. package/dist/tools/registry.js.map +1 -1
  231. package/dist/tools/shell.d.ts +3 -0
  232. package/dist/tools/shell.js +35 -13
  233. package/dist/tools/shell.js.map +1 -1
  234. package/dist/tools/tool-types.d.ts +11 -0
  235. package/dist/tools/web/fetch-core.d.ts +7 -0
  236. package/dist/tools/web/fetch-core.js +40 -1
  237. package/dist/tools/web/fetch-core.js.map +1 -1
  238. package/dist/tools/web/fetch.js +8 -1
  239. package/dist/tools/web/fetch.js.map +1 -1
  240. package/dist/tui-v2/app/App.js +34 -41
  241. package/dist/tui-v2/app/App.js.map +1 -1
  242. package/dist/tui-v2/app/command-handlers.js +20 -1
  243. package/dist/tui-v2/app/command-handlers.js.map +1 -1
  244. package/dist/tui-v2/app/commands/picker-commands.js +3 -0
  245. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  246. package/dist/tui-v2/app/commands/session-commands.js +5 -0
  247. package/dist/tui-v2/app/commands/session-commands.js.map +1 -1
  248. package/dist/tui-v2/app/plan-lifecycle.d.ts +25 -3
  249. package/dist/tui-v2/app/plan-lifecycle.js +87 -24
  250. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
  251. package/dist/tui-v2/bootstrap/patch-opentui-text.d.ts +6 -0
  252. package/dist/tui-v2/bootstrap/patch-opentui-text.js +53 -0
  253. package/dist/tui-v2/bootstrap/patch-opentui-text.js.map +1 -0
  254. package/dist/tui-v2/bootstrap/start-tui-v2.js +7 -0
  255. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  256. package/dist/tui-v2/components/jobs/jobs-panel.js +23 -14
  257. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  258. package/dist/tui-v2/components/modal/confirm-modal.d.ts +1 -1
  259. package/dist/tui-v2/components/modal/confirm-modal.js +18 -7
  260. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
  261. package/dist/tui-v2/components/overlay/overlay-host.js +1 -1
  262. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  263. package/dist/tui-v2/components/pager/pager.d.ts +6 -0
  264. package/dist/tui-v2/components/pager/pager.js +313 -67
  265. package/dist/tui-v2/components/pager/pager.js.map +1 -1
  266. package/dist/tui-v2/components/status/status-line.d.ts +9 -0
  267. package/dist/tui-v2/components/status/status-line.js +17 -3
  268. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  269. package/dist/tui-v2/components/transcript/assistant-message.js +1 -1
  270. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  271. package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
  272. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  273. package/dist/tui-v2/components/transcript/tool-card.d.ts +9 -1
  274. package/dist/tui-v2/components/transcript/tool-card.js +114 -9
  275. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  276. package/dist/tui-v2/components/transcript/transcript-row.js +2 -2
  277. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  278. package/dist/tui-v2/composer/composer-editor.js +16 -0
  279. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  280. package/dist/tui-v2/controllers/overlay-controller.d.ts +16 -2
  281. package/dist/tui-v2/controllers/overlay-controller.js +92 -12
  282. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
  283. package/dist/tui-v2/rendering/artifact-pager-source.d.ts +19 -0
  284. package/dist/tui-v2/rendering/artifact-pager-source.js +103 -0
  285. package/dist/tui-v2/rendering/artifact-pager-source.js.map +1 -0
  286. package/dist/tui-v2/rendering/file-diff-view.d.ts +63 -0
  287. package/dist/tui-v2/rendering/file-diff-view.js +263 -0
  288. package/dist/tui-v2/rendering/file-diff-view.js.map +1 -0
  289. package/dist/tui-v2/rendering/open-tool-output.d.ts +9 -1
  290. package/dist/tui-v2/rendering/open-tool-output.js +65 -12
  291. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
  292. package/dist/tui-v2/rendering/pager-chrome.d.ts +12 -0
  293. package/dist/tui-v2/rendering/pager-chrome.js +56 -0
  294. package/dist/tui-v2/rendering/pager-chrome.js.map +1 -0
  295. package/dist/tui-v2/rendering/plan-view.js +1 -1
  296. package/dist/tui-v2/rendering/plan-view.js.map +1 -1
  297. package/dist/tui-v2/rendering/syntax-highlight.d.ts +37 -0
  298. package/dist/tui-v2/rendering/syntax-highlight.js +1303 -0
  299. package/dist/tui-v2/rendering/syntax-highlight.js.map +1 -0
  300. package/dist/tui-v2/rendering/theme.d.ts +28 -0
  301. package/dist/tui-v2/rendering/theme.js +28 -0
  302. package/dist/tui-v2/rendering/theme.js.map +1 -1
  303. package/dist/tui-v2/rendering/tool-presenter.d.ts +5 -0
  304. package/dist/tui-v2/rendering/tool-presenter.js +35 -3
  305. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
  306. package/dist/tui-v2/state/transcript-hydrate.js +5 -0
  307. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  308. package/dist/tui-v2/state/transcript-reducer.js +12 -2
  309. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  310. package/dist/tui-v2/state/transcript-store.d.ts +7 -0
  311. package/dist/tui-v2/state/transcript-store.js +46 -0
  312. package/dist/tui-v2/state/transcript-store.js.map +1 -1
  313. package/dist/tui-v2/state/transcript-types.d.ts +12 -0
  314. package/dist/tui-v2/state/transcript-types.js +9 -0
  315. package/dist/tui-v2/state/transcript-types.js.map +1 -1
  316. package/dist/types.d.ts +17 -1
  317. package/dist/ui/mentions.d.ts +6 -0
  318. package/dist/ui/mentions.js +50 -12
  319. package/dist/ui/mentions.js.map +1 -1
  320. package/dist/ui/plan-pane.js +1 -1
  321. package/dist/ui/plan-pane.js.map +1 -1
  322. package/package.json +18 -12
@@ -1,6 +1,19 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { getActiveProjectRoot } from "./project-root.js";
4
+ /** Rehydrate the live task ledger from durable plan evidence after a resume. */
5
+ export function ledgerFromTaskEvidence(taskId, evidence) {
6
+ return {
7
+ taskId,
8
+ successWorkCount: evidence?.successWorkCount ?? 0,
9
+ ...(evidence ?? {}),
10
+ };
11
+ }
12
+ /** Store only serializable evidence on the plan; the task id is already its key. */
13
+ export function taskEvidenceFromLedger(ledger) {
14
+ const { taskId: _taskId, ...evidence } = ledger;
15
+ return evidence;
16
+ }
4
17
  export function isMetaPlanTool(name) {
5
18
  return name === "plan.create" || name === "task.update" || name === "agent.handoff";
6
19
  }
@@ -8,23 +21,256 @@ export function isMetaPlanTool(name) {
8
21
  export function isEvidenceWorkTool(name) {
9
22
  return !isMetaPlanTool(name);
10
23
  }
11
- export function recordTaskWorkSuccess(ledger, taskId, toolName) {
12
- if (!taskId || !isEvidenceWorkTool(toolName))
24
+ export function isPentestPlanKind(kind) {
25
+ return (kind ?? "").toLowerCase() === "pentest";
26
+ }
27
+ export function classifyTaskTitle(title, opts) {
28
+ const t = title.toLowerCase();
29
+ const pentest = isPentestPlanKind(opts?.planKind);
30
+ // Pentest plans: never map security work onto coding verify/implement gates.
31
+ if (pentest) {
32
+ if (/\b(report|write.?up|finding|summar|document|residual)\b/.test(t)) {
33
+ return "report";
34
+ }
35
+ if (/\b(exploit|poc|payload|privesc|lateral|inject|bruteforce|sqlmap|listener|reverse\s+shell|c2)\b/.test(t)) {
36
+ return "exploit";
37
+ }
38
+ if (/\b(recon|enumerat|fingerprint|osint|whois|dns|nmap|discover|probe|scan|fuzz|content\s+discover|subdomain|port)\b/.test(t)) {
39
+ return "recon";
40
+ }
41
+ if (/\b(explore|inspect|list|read|survey|map)\b/.test(t)) {
42
+ return "explore";
43
+ }
44
+ return "generic";
45
+ }
46
+ if (/\b(dev\s*server|run\s+dev|start\s+.*server|localhost|shell\.start|leave\s+.*running|probe|verify\s+in\s+browser)\b/.test(t) ||
47
+ (/\b(start|run)\b/.test(t) && /\b(server|dev|app|verify)\b/.test(t))) {
48
+ return "verify";
49
+ }
50
+ if (looksLikeInstallTaskTitle(title))
51
+ return "install";
52
+ if (looksLikeScaffoldTaskTitle(title))
53
+ return "scaffold";
54
+ if (/\b(implement|integrate|feature|rewrite|component|todo|persist|localstorage|styling|styles?|ui|page)\b/.test(t) ||
55
+ // endpoint/route only count as implement for product apps, not bare "API routes" recon phrasing
56
+ (/\b(endpoint|route)\b/.test(t) &&
57
+ /\b(implement|build|add|create|feature|component|ui|page)\b/.test(t))) {
58
+ return "implement";
59
+ }
60
+ if (/\b(recon|enumerat|fingerprint|osint|whois|dns|nmap|discover)\b/.test(t)) {
61
+ return "recon";
62
+ }
63
+ if (/\b(exploit|poc|payload|privesc|lateral|inject|bruteforce|sqlmap)\b/.test(t)) {
64
+ return "exploit";
65
+ }
66
+ if (/\b(report|write.?up|finding|summar|document)\b/.test(t)) {
67
+ return "report";
68
+ }
69
+ if (/\b(explore|inspect|list|read|survey|map)\b/.test(t) ||
70
+ /\bcheck\b.+\b(exists?|empty|directory|folder|path)\b/.test(t) ||
71
+ /\b(exists?|empty)\b.+\b(directory|folder|path|project)\b/.test(t) ||
72
+ // "Check Node.js and npm availability", "verify tools present", etc.
73
+ /\bcheck\b.+\b(node|npm|pnpm|yarn|bun|python|availability|available|installed|present|toolchain|tools?)\b/.test(t) ||
74
+ /\b(availability|available|installed|present)\b.+\b(node|npm|toolchain|tools?)\b/.test(t) ||
75
+ /\bverify\b.+\b(tools?|node|npm|setup|environment|prereq)\b/.test(t)) {
76
+ return "explore";
77
+ }
78
+ return "generic";
79
+ }
80
+ /**
81
+ * Whether a successful tool can satisfy a task of the given class.
82
+ * Used to retroactively credit preflight work (tool.check before in_progress).
83
+ */
84
+ export function toolFitsTaskClass(toolName, taskTitle, opts) {
85
+ if (!isEvidenceWorkTool(toolName))
86
+ return false;
87
+ const cls = classifyTaskTitle(taskTitle, { planKind: opts?.planKind });
88
+ const s = opts?.signals;
89
+ switch (cls) {
90
+ case "explore":
91
+ return (toolName === "tool.check" ||
92
+ toolName === "sysinfo" ||
93
+ toolName === "fs.list" ||
94
+ toolName === "fs.read" ||
95
+ toolName === "fs.search" ||
96
+ toolName === "net.context" ||
97
+ toolName === "shell.exec" // version probes, which/where
98
+ );
99
+ case "scaffold":
100
+ return (Boolean(s?.scaffoldOk) ||
101
+ toolName === "fs.write" ||
102
+ toolName === "fs.writeMany" ||
103
+ toolName === "shell.exec" ||
104
+ toolName === "shell.start");
105
+ case "install":
106
+ return Boolean(s?.installOk) || toolName === "pkg.install";
107
+ case "implement":
108
+ return (Boolean(s?.sourceWrite || s?.featureWrite) ||
109
+ toolName === "fs.write" ||
110
+ toolName === "fs.writeMany" ||
111
+ toolName === "fs.edit" ||
112
+ toolName === "fs.replaceLines" ||
113
+ toolName === "fs.append");
114
+ case "verify":
115
+ return (Boolean(s?.devServerStart ||
116
+ s?.localHttpProbeOk ||
117
+ s?.serverReady ||
118
+ s?.portListening) ||
119
+ toolName === "shell.start" ||
120
+ toolName === "shell.tail" ||
121
+ toolName === "http.fetch" ||
122
+ toolName === "web.fetch");
123
+ case "recon":
124
+ return (Boolean(s?.remoteReconOk) ||
125
+ /^(dns\.lookup|whois\.lookup|http\.fetch|web\.fetch|net\.scan|pentest\.recon|net\.pingSweep|net\.context|tool\.batch)$/.test(toolName));
126
+ case "exploit":
127
+ return Boolean(s?.remoteActiveTestOk) || isEvidenceWorkTool(toolName);
128
+ default:
129
+ return true;
130
+ }
131
+ }
132
+ /**
133
+ * Fold turn-level loose successes into a task ledger so preflight work
134
+ * (tool.check / fs.list before in_progress) can satisfy explore/check tasks.
135
+ */
136
+ export function absorbLooseWorkIntoLedger(ledger, taskId, taskTitle, loose, opts) {
137
+ if (!taskId || loose.length === 0)
13
138
  return ledger;
14
- if (!ledger || ledger.taskId !== taskId) {
15
- return { taskId, successWorkCount: 1, lastOkTool: toolName };
139
+ let led = ledger;
140
+ for (const item of loose) {
141
+ if (!toolFitsTaskClass(item.toolName, taskTitle, {
142
+ planKind: opts?.planKind,
143
+ signals: item.signals,
144
+ })) {
145
+ continue;
146
+ }
147
+ led = recordTaskWorkSuccess(led, taskId, item.toolName, item.signals);
16
148
  }
17
- return {
18
- taskId,
19
- successWorkCount: ledger.successWorkCount + 1,
20
- lastOkTool: toolName,
21
- };
149
+ return led;
150
+ }
151
+ export function recordTaskWorkSuccess(ledger, taskId, toolName, signals) {
152
+ if (!taskId || !isEvidenceWorkTool(toolName))
153
+ return ledger;
154
+ const base = !ledger || ledger.taskId !== taskId
155
+ ? { taskId, successWorkCount: 1, lastOkTool: toolName }
156
+ : {
157
+ ...ledger,
158
+ successWorkCount: ledger.successWorkCount + 1,
159
+ lastOkTool: toolName,
160
+ };
161
+ if (signals?.sourceWrite)
162
+ base.sawSourceWrite = true;
163
+ if (signals?.featureWrite)
164
+ base.sawFeatureWrite = true;
165
+ if (signals?.installOk)
166
+ base.sawInstallOk = true;
167
+ if (signals?.scaffoldOk)
168
+ base.sawScaffoldOk = true;
169
+ if (signals?.devServerStart)
170
+ base.sawDevServerStart = true;
171
+ if (signals?.localHttpProbeOk)
172
+ base.sawLocalHttpProbeOk = true;
173
+ if (signals?.serverReady)
174
+ base.sawServerReady = true;
175
+ if (signals?.portListening)
176
+ base.sawPortListening = true;
177
+ if (signals?.remoteReconOk)
178
+ base.sawRemoteReconOk = true;
179
+ if (signals?.remoteActiveTestOk)
180
+ base.sawRemoteActiveTestOk = true;
181
+ return base;
22
182
  }
23
183
  export function openTaskLedger(taskId) {
24
184
  return { taskId, successWorkCount: 0 };
25
185
  }
26
- export function canMarkTaskDone(ledger, taskId) {
27
- if (!ledger || ledger.taskId !== taskId) {
186
+ /** Local app runtime is proven by any strong independent signal. */
187
+ export function hasLocalRuntimeProof(evidence) {
188
+ return Boolean(evidence?.sawDevServerStart ||
189
+ evidence?.sawLocalHttpProbeOk ||
190
+ evidence?.sawServerReady ||
191
+ evidence?.sawPortListening);
192
+ }
193
+ export function hasRemoteWorkProof(evidence) {
194
+ return Boolean(evidence?.sawRemoteReconOk || evidence?.sawRemoteActiveTestOk);
195
+ }
196
+ /** Dev-server job log shows ready + local URL. */
197
+ export function isServerReadyOutput(output) {
198
+ if (!output)
199
+ return false;
200
+ const hasUrl = /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\b/i.test(output) ||
201
+ /➜\s*Local:\s*https?:\/\//i.test(output);
202
+ const hasReady = /\b(?:ready in|VITE\s+v?\d|compiled successfully|started server|listening on|Local:\s*http)\b/i.test(output);
203
+ return hasUrl || hasReady;
204
+ }
205
+ /** lsof/ss (or similar) shows a process LISTENing on a local port. */
206
+ export function isPortListeningOutput(command, output) {
207
+ if (!output || !/\bLISTEN(?:ING)?\b/i.test(output))
208
+ return false;
209
+ const cmd = command.toLowerCase();
210
+ if (/\b(lsof|ss\b|netstat)\b/.test(cmd))
211
+ return true;
212
+ // Output-only: node/… LISTEN with a localhost/port line
213
+ return (/\b(?:localhost|127\.0\.0\.1|\[::1\]|\*:)\S*\d+/i.test(output) ||
214
+ /\bTCP\b.+\bLISTEN/i.test(output));
215
+ }
216
+ /**
217
+ * Leave/keep-running observational tasks may inherit already-proven runtime.
218
+ * "for user to test" must NOT block inheritance (prior brittle exclusion of "test").
219
+ */
220
+ export function isRuntimeObservationTask(title) {
221
+ const t = title.toLowerCase();
222
+ const leaveKeep = /\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bserver\s+(?:is\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/.test(t);
223
+ if (!leaveKeep)
224
+ return false;
225
+ // Primary action is still start/launch/probe → not pure observation
226
+ if (/\b(?:start|launch|run)\s+(?:the\s+)?(?:dev\s*)?server\b/.test(t) ||
227
+ /\b(?:probe|curl)\s+(?:localhost|http)\b/.test(t)) {
228
+ // Combined "start … leave running" is verify work, not pure observation
229
+ if (/\b(start|run|launch)\b/.test(t) && /\b(dev\s*server|npm\s+run\s+dev|shell\.start)\b/.test(t)) {
230
+ return false;
231
+ }
232
+ }
233
+ // Titles that are ONLY leave/keep running (optionally "for user to test")
234
+ if (/\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/.test(t)) {
235
+ // If title is primarily leave-running without requiring a fresh start as the main verb first
236
+ if (!/^(?:start|run|launch)\b/.test(t.trim()))
237
+ return true;
238
+ // "Leave server running for user to test" — leave is the head intent
239
+ if (/^leave\b|^keep\b/.test(t.trim()))
240
+ return true;
241
+ }
242
+ return (/\bserver\s+(?:is\s+)?running\b/.test(t) &&
243
+ !/\b(?:start|run|launch)\b/.test(t));
244
+ }
245
+ /** Report / residual documentation that can inherit prior remote evidence. */
246
+ export function isRemoteObservationTask(title) {
247
+ const t = title.toLowerCase();
248
+ if (/\b(report|write.?up|summar|document|residual|findings?\s+summary)\b/.test(t)) {
249
+ return true;
250
+ }
251
+ if (/\bleave\s+(?:the\s+)?(?:listener|job|tunnel)\s+running\b|\bkeep\s+(?:the\s+)?listener\b/.test(t)) {
252
+ return true;
253
+ }
254
+ return false;
255
+ }
256
+ /**
257
+ * Typed evidence gate: base rule is ≥1 successful work tool; implement/install/verify
258
+ * (and pentest recon) require stronger signals when classified. Existing project and
259
+ * already-proven runtime/remote facts satisfy corresponding observational tasks.
260
+ */
261
+ export function canMarkTaskDone(ledger, taskId, opts) {
262
+ const title = opts?.taskTitle ?? "";
263
+ const planKind = opts?.planKind;
264
+ const pentest = isPentestPlanKind(planKind);
265
+ const cls = title ? classifyTaskTitle(title, { planKind }) : "generic";
266
+ const leaveRunningIntent = isRuntimeObservationTask(title) ||
267
+ /\bleave\s+(?:it\s+)?running\b|\bkeep\s+(?:it\s+)?running\b|\bfor\s+(?:the\s+)?user\s+to\s+test\b/i.test(title);
268
+ const inheritedCompletion = (cls === "scaffold" && Boolean(opts?.existingProject)) ||
269
+ (!pentest && leaveRunningIntent && Boolean(opts?.runtimeVerified)) ||
270
+ (pentest &&
271
+ isRemoteObservationTask(title) &&
272
+ Boolean(opts?.remoteWorkVerified));
273
+ if ((!ledger || ledger.taskId !== taskId) && !inheritedCompletion) {
28
274
  return {
29
275
  ok: false,
30
276
  reason: `Cannot mark [${taskId}] done: no work ledger for this task. ` +
@@ -32,18 +278,113 @@ export function canMarkTaskDone(ledger, taskId) {
32
278
  `inspect the tool result, and only then mark done if you are satisfied.`,
33
279
  };
34
280
  }
35
- if (ledger.successWorkCount < 1) {
281
+ if ((ledger?.successWorkCount ?? 0) < 1 && !inheritedCompletion) {
36
282
  return {
37
283
  ok: false,
38
284
  reason: `Cannot mark [${taskId}] done: no successful tool result observed since it went in_progress. ` +
39
285
  `Do the work, wait for the tool output, verify it succeeded, and only mark done when satisfied. ` +
40
- (ledger.lastOkTool
286
+ (ledger?.lastOkTool
41
287
  ? ""
42
288
  : "Example: fs.write / shell.exec / shell.start must return ok first."),
43
289
  };
44
290
  }
291
+ const evidence = ledger && ledger.taskId === taskId ? ledger : openTaskLedger(taskId);
292
+ // Feature implement gate is coding-only.
293
+ if (!pentest && cls === "implement" && opts?.featureAppRequired) {
294
+ const featureOk = Boolean(evidence.sawFeatureWrite) || Boolean(opts.sessionFeatureSeen);
295
+ if (!featureOk) {
296
+ return {
297
+ ok: false,
298
+ reason: `Cannot mark [${taskId}] done: this is a feature/implement task for a product app, ` +
299
+ `but no real feature code was written yet (starter boilerplate does not count). ` +
300
+ `Replace the default starter with the requested feature (e.g. todo add/list/toggle), then mark done.`,
301
+ };
302
+ }
303
+ }
304
+ if (!pentest && cls === "install" && !evidence.sawInstallOk) {
305
+ if (evidence.successWorkCount < 1) {
306
+ return {
307
+ ok: false,
308
+ reason: `Cannot mark [${taskId}] done: install task needs a successful package install ` +
309
+ `(npm/pnpm/yarn/bun install, etc.) under this task.`,
310
+ };
311
+ }
312
+ }
313
+ // Local app runtime: multi-signal proof. Never apply to pentest plans.
314
+ if (!pentest && (cls === "verify" || leaveRunningIntent)) {
315
+ const wantsServer = leaveRunningIntent ||
316
+ /\b(dev\s*server|localhost|shell\.start|run\s+dev|leave\s+.*running)\b/i.test(title);
317
+ if (wantsServer) {
318
+ const proof = hasLocalRuntimeProof(evidence) || Boolean(opts?.runtimeVerified);
319
+ if (!proof) {
320
+ return {
321
+ ok: false,
322
+ reason: `Cannot mark [${taskId}] done: local app runtime not proven yet. ` +
323
+ `Accept any of: shell.start (dev server), shell.tail ready+URL, port LISTEN (lsof/ss), ` +
324
+ `or successful localhost GET probe. If an earlier plan task already proved the server, ` +
325
+ `confirm it is still alive once then mark done — do not thrash ports with restarts.`,
326
+ };
327
+ }
328
+ }
329
+ }
330
+ // Pentest recon: need real remote recon, not local fs noise alone.
331
+ if (pentest && cls === "recon") {
332
+ const remoteOk = hasRemoteWorkProof(evidence) || Boolean(opts?.remoteWorkVerified);
333
+ if (!remoteOk) {
334
+ // Allow if last tool was a recon tool name even without typed flag (soft)
335
+ const last = evidence.lastOkTool ?? "";
336
+ const lastLooksRecon = /^(dns\.lookup|whois\.lookup|http\.fetch|web\.fetch|net\.scan|pentest\.recon|net\.pingSweep|tool\.batch)$/.test(last);
337
+ if (!lastLooksRecon) {
338
+ return {
339
+ ok: false,
340
+ reason: `Cannot mark [${taskId}] done: recon needs successful remote evidence ` +
341
+ `(dns/http/net.scan/pentest.recon/whois against the target), not local-only tools.`,
342
+ };
343
+ }
344
+ }
345
+ }
45
346
  return { ok: true };
46
347
  }
348
+ /** True when a tool call is remote recon (not local app tooling). */
349
+ export function isRemoteReconToolCall(call) {
350
+ if (call.name === "dns.lookup" ||
351
+ call.name === "whois.lookup" ||
352
+ call.name === "net.scan" ||
353
+ call.name === "pentest.recon" ||
354
+ call.name === "net.pingSweep" ||
355
+ call.name === "net.context") {
356
+ return true;
357
+ }
358
+ if (call.name === "http.fetch" || call.name === "web.fetch") {
359
+ const url = typeof call.args.url === "string" ? call.args.url : "";
360
+ // Localhost is coding probe, not remote recon
361
+ if (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/i.test(url)) {
362
+ return false;
363
+ }
364
+ return Boolean(url);
365
+ }
366
+ if (call.name === "shell.exec" || call.name === "shell.start") {
367
+ const cmd = commandOf(call);
368
+ return /\b(?:nmap|masscan|ffuf|gobuster|feroxbuster|nikto|nuclei|httpx|subfinder|amass|dig|whois|whatweb)\b/i.test(cmd);
369
+ }
370
+ return false;
371
+ }
372
+ /** Active/offensive test (not passive recon). */
373
+ export function isRemoteActiveTestCall(call) {
374
+ if (call.name === "http.fetch") {
375
+ const method = String(call.args.method ?? "GET").toUpperCase();
376
+ if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
377
+ return true;
378
+ }
379
+ const url = typeof call.args.url === "string" ? call.args.url : "";
380
+ return /\b(?:union\s+select|<\s*script|javascript:|file:\/\/|169\.254\.169\.254)\b/i.test(url);
381
+ }
382
+ if (call.name === "shell.exec" || call.name === "shell.start") {
383
+ const cmd = commandOf(call);
384
+ return /\b(?:sqlmap|hydra|nikto|nuclei|msfconsole|exploit|payload|reverse\s+shell)\b/i.test(cmd);
385
+ }
386
+ return false;
387
+ }
47
388
  function commandOf(call) {
48
389
  return typeof call.args.command === "string" ? call.args.command : "";
49
390
  }
@@ -70,7 +411,7 @@ export function isDevServerCall(call) {
70
411
  }
71
412
  return (/\bnpm\s+run\s+dev\b|\byarn\s+dev\b|\bpnpm\s+(run\s+)?dev\b|\bbun\s+(run\s+)?dev\b|\bnext\s+dev\b|\bnuxt\s+dev\b|\brails\s+s(?:erver)?\b|\bdjango(-admin)?\s+runserver\b/i.test(cmd) || /(?:^|[;&|]\s*)vite(?:\s|$)/i.test(cmd));
72
413
  }
73
- /** Preflight / inspect tools allowed before the model opens a task. */
414
+ /** Inspect tools allowed without opening a plan task. */
74
415
  export function isPlanPreflightTool(name) {
75
416
  return (name === "tool.check" ||
76
417
  name === "sysinfo" ||
@@ -79,16 +420,45 @@ export function isPlanPreflightTool(name) {
79
420
  name === "fs.search" ||
80
421
  name === "net.context");
81
422
  }
423
+ /** Read-only recon tools that may run without an in_progress task on pentest plans. */
424
+ export function isReadOnlyReconTool(name) {
425
+ return (isPlanPreflightTool(name) ||
426
+ name === "tool.batch" ||
427
+ name === "dns.lookup" ||
428
+ name === "whois.lookup" ||
429
+ name === "http.fetch" ||
430
+ name === "web.fetch" ||
431
+ name === "web.search" ||
432
+ name === "net.scan" ||
433
+ name === "pentest.recon" ||
434
+ name === "wordlist.find");
435
+ }
82
436
  /**
83
- * Tools allowed on a coding BUILD turn before plan.create exists.
84
- * Explore + optional web research + plan — no scaffold/write/install freestyle.
437
+ * Tools whose failure must NOT cancel later siblings in the same model turn.
438
+ * Plan bookkeeping failures (task.update done-gate, etc.) and independent
439
+ * recon lookups should never wipe a whole batch of http.fetch / dns calls.
85
440
  */
441
+ export function isBatchSoftFailTool(name) {
442
+ if (name === "plan.create" || name === "task.update")
443
+ return true;
444
+ if (name === "tool.batch" || name === "tool.check")
445
+ return true;
446
+ if (isReadOnlyReconTool(name))
447
+ return true;
448
+ if (name === "net.pingSweep" ||
449
+ name === "shell.jobs" ||
450
+ name === "shell.tail" ||
451
+ name === "sysinfo") {
452
+ return true;
453
+ }
454
+ return false;
455
+ }
456
+ /** Tools allowed on a coding build turn before plan.create exists. */
86
457
  export function isBuildPrePlanAllowedTool(name) {
87
458
  return (name === "plan.create" ||
88
459
  isPlanPreflightTool(name) ||
89
460
  name === "tool.batch" ||
90
461
  name === "fs.search" ||
91
- // Research before planning (docs/examples) is fine; it does not mutate the project
92
462
  name === "web.search" ||
93
463
  name === "web.fetch");
94
464
  }
@@ -120,8 +490,47 @@ export function toolStallBudgetMs(call) {
120
490
  isLongQuietInstallOrScaffoldCommand(cmd)) {
121
491
  return 15 * 60_000; // create-next-app + npm install often 2–10+ min with quiet stretches
122
492
  }
493
+ // Network tools should not sit silent for a full minute — their own
494
+ // per-request timeouts are shorter; 45s is a safety net for hung sockets.
495
+ if (call.name === "web.search" || call.name === "web.fetch" || call.name === "http.fetch") {
496
+ return 45_000;
497
+ }
123
498
  return 60_000;
124
499
  }
500
+ /**
501
+ * Hard wall-clock budget for a tool call. When exceeded the runner aborts
502
+ * and force-settles even if the underlying promise never resolves (hung
503
+ * socket that ignored AbortSignal). Distinct from the stall watchdog
504
+ * (which fires on *silence*); this fires on total elapsed time.
505
+ */
506
+ export function toolHardBudgetMs(call) {
507
+ const cmd = typeof call.args.command === "string" ? call.args.command : "";
508
+ if (call.name === "pkg.install")
509
+ return 20 * 60_000;
510
+ if ((call.name === "shell.exec" || call.name === "shell.start") &&
511
+ isLongQuietInstallOrScaffoldCommand(cmd)) {
512
+ return 20 * 60_000;
513
+ }
514
+ if (call.name === "web.search") {
515
+ // search (15s) + up to 3 fetchTop pages (30s each) + margin
516
+ const fetchTop = typeof call.args.fetchTop === "number" && Number.isFinite(call.args.fetchTop)
517
+ ? Math.max(0, Math.min(3, Math.floor(call.args.fetchTop)))
518
+ : 0;
519
+ return 15_000 + fetchTop * 30_000 + 15_000;
520
+ }
521
+ if (call.name === "web.fetch")
522
+ return 45_000;
523
+ if (call.name === "http.fetch")
524
+ return 90_000;
525
+ if (call.name === "net.scan" || call.name === "pentest.recon")
526
+ return 15 * 60_000;
527
+ if (call.name === "shell.start")
528
+ return 120_000; // should background quickly
529
+ // Default hard cap so a silent hung tool cannot freeze the session forever.
530
+ return 5 * 60_000;
531
+ }
532
+ /** Grace period after abort before force-settling a hung tool promise. */
533
+ export const TOOL_ABORT_GRACE_MS = 2_500;
125
534
  /** Harmless version/which probes models use instead of tool.check before planning. */
126
535
  export function isReadOnlyVersionProbeCommand(cmd) {
127
536
  const c = cmd.trim();
@@ -170,67 +579,18 @@ export function looksLikeScaffoldTaskTitle(title) {
170
579
  export function isPackageInstallCommand(cmd) {
171
580
  return /\bnpm\s+i(nstall)?\b|\byarn\s+install\b|\bpnpm\s+i(nstall)?\b|\bbun\s+install\b|\bpip\s+install\b|\bpoetry\s+install\b|\bcargo\s+build\b|\bcomposer\s+install\b|\bbundle\s+install\b|\bgo\s+mod\s+tidy\b/.test(cmd);
172
581
  }
173
- /**
174
- * Soft scope check: refuse run/verify tools while the open task is only
175
- * scaffold / implement / install.
176
- * When `planTaskTitles` is provided, install is only blocked on implement
177
- * tasks if the plan actually has a dedicated install task (otherwise allow).
178
- */
179
- export function workOutOfScopeForTask(taskTitle, call, opts) {
180
- if (isMetaPlanTool(call.name))
181
- return undefined;
182
- const t = taskTitle.toLowerCase();
183
- const isRunVerifyTask = /\b(dev\s*server|run\s+dev|start\s+.*server|localhost|shell\.start|leave\s+.*running|probe|verify\s+in\s+browser)\b/.test(t) ||
184
- (/\b(start|run)\b/.test(t) && /\b(server|dev|app|verify)\b/.test(t));
185
- if (isRunVerifyTask)
186
- return undefined;
187
- const isInstallOnly = looksLikeInstallTaskTitle(taskTitle);
188
- const isScaffoldOnly = looksLikeScaffoldTaskTitle(taskTitle);
189
- const isImplementOnly = !isInstallOnly &&
190
- !isScaffoldOnly &&
191
- (/\b(implement|integrate|feature|rewrite|write\s+src|add\s+\w+\s+(component|module|handler|endpoint|page|route|model)|persist|localstorage|styling|styles?)\b/.test(t) ||
192
- (/\b(component|module|handler|endpoint|ui|page|todo)\b/.test(t) &&
193
- !/\b(install|scaffold|dev\s*server|create\s+project)\b/.test(t)));
194
- // Install always OK under scaffold or install tasks
195
- if ((isScaffoldOnly || isInstallOnly) &&
196
- call.name === "shell.exec" &&
197
- isPackageInstallCommand(commandOf(call))) {
198
- return undefined;
199
- }
200
- if ((isInstallOnly || isScaffoldOnly || isImplementOnly) &&
201
- (isDevServerCall(call) || isLocalHttpProbe(call))) {
202
- return (`Out of scope for current task "${taskTitle}": do not start the dev server or probe localhost yet. ` +
203
- `Finish and VERIFY this task's own work, mark it done only when satisfied, then open the run/verify task with task.update in_progress.`);
204
- }
205
- if (isImplementOnly &&
206
- call.name === "shell.exec" &&
207
- isPackageInstallCommand(commandOf(call))) {
208
- const planTitles = opts?.planTaskTitles;
209
- const planHasInstallTask = !planTitles ||
210
- planTitles.length === 0 ||
211
- planTitles.some((title) => looksLikeInstallTaskTitle(title));
212
- if (planHasInstallTask) {
213
- return (`Out of scope for current task "${taskTitle}": run package install under the install task, not while implementing files.`);
214
- }
215
- // No install task in the plan — allow install so work is not stuck.
216
- return undefined;
217
- }
218
- return undefined;
219
- }
220
582
  /**
221
583
  * Choose which pending plan task should own this tool call (auto-start).
222
584
  * Prevents npm install from being attributed to "localStorage" / style tasks.
223
585
  */
224
- export function pickPendingTaskForToolCall(pending, call, allTitles) {
586
+ export function pickPendingTaskForToolCall(pending, call, _allTitles) {
225
587
  if (pending.length === 0)
226
588
  return undefined;
227
- const titles = allTitles ?? pending.map((p) => p.title);
228
589
  const cmd = commandOf(call);
229
590
  const score = (title) => {
230
- if (workOutOfScopeForTask(title, call, { planTaskTitles: titles })) {
231
- return -100;
232
- }
233
- let s = 1; // any in-scope pending task is viable
591
+ // Heuristics rank likely ownership only. They must never make a pending
592
+ // task ineligible or authorize/block the underlying tool call.
593
+ let s = 1;
234
594
  if (isPackageInstallCommand(cmd)) {
235
595
  if (looksLikeInstallTaskTitle(title))
236
596
  s += 20;
@@ -255,6 +615,19 @@ export function pickPendingTaskForToolCall(pending, call, allTitles) {
255
615
  s += 12;
256
616
  }
257
617
  }
618
+ // Keyword overlap (SSRF task ↔ /api/og-image fetch, fuzz ↔ /api/*, …)
619
+ const blob = `${call.name} ${cmd} ${JSON.stringify(call.args ?? {})}`.toLowerCase();
620
+ const words = title
621
+ .toLowerCase()
622
+ .split(/[^a-z0-9]+/)
623
+ .filter((w) => w.length > 3);
624
+ let hits = 0;
625
+ for (const w of words) {
626
+ if (blob.includes(w))
627
+ hits += 1;
628
+ }
629
+ if (hits > 0)
630
+ s += Math.min(12, hits * 3);
258
631
  return s;
259
632
  };
260
633
  let best;
@@ -266,9 +639,7 @@ export function pickPendingTaskForToolCall(pending, call, allTitles) {
266
639
  bestScore = sc;
267
640
  }
268
641
  }
269
- if (!best || bestScore < 0)
270
- return undefined;
271
- return best;
642
+ return best ?? pending[0];
272
643
  }
273
644
  /**
274
645
  * User asked for a product feature app (todo, blog, …), not "just scaffold framework X".
@@ -321,33 +692,90 @@ export function extractWritePathsFromCall(call) {
321
692
  }
322
693
  return paths;
323
694
  }
695
+ /** Default framework starter / marketing copy — not the product feature. */
696
+ export function looksLikeStarterBoilerplate(content) {
697
+ const c = content.slice(0, 8_000);
698
+ return (/\bWelcome to (?:Vite|React|Next\.js|Create React App|Nuxt|SvelteKit|Angular)\b/i.test(c) ||
699
+ /\bGet started by editing\b/i.test(c) ||
700
+ /\bEdit\s+`?(?:src\/)?App\.(?:tsx?|jsx?)`?\s+and save to (?:test|reload)/i.test(c) ||
701
+ /\blogos?\/(?:react|vite|next)\.svg\b/i.test(c) ||
702
+ /\bnext\.js logo\b/i.test(c) ||
703
+ /https?:\/\/(?:react\.dev|vitejs\.dev|nextjs\.org)\/(?:learn|docs)?/i.test(c) && /\b(?:Deploy|Learn|Templates|create-next-app)\b/i.test(c));
704
+ }
705
+ /** Content that looks like real product UI/logic for common feature apps. */
706
+ export function looksLikeFeatureProductCode(content) {
707
+ const c = content.slice(0, 12_000);
708
+ if (looksLikeStarterBoilerplate(c))
709
+ return false;
710
+ // Interactive product signals (todo/crud/auth/etc.)
711
+ if (/\b(?:useState|useReducer|createContext|signal\(|createSignal|ref\()\b/.test(c) &&
712
+ /\b(?:todo|todos|onClick|onSubmit|addTodo|toggle|complete|delete|remove|login|signup|password|fetch\(|axios|localStorage)\b/i.test(c)) {
713
+ return true;
714
+ }
715
+ if (/\b(?:add|create|toggle|delete|remove|update)\b/i.test(c) &&
716
+ /\b(?:todo|item|task|post|note|user|cart)\b/i.test(c) &&
717
+ /\b(?:map\s*\(|filter\s*\(|set[A-Z]\w+|useState)\b/.test(c)) {
718
+ return true;
719
+ }
720
+ // API/backend feature
721
+ if (/\b(?:app\.(?:get|post|put|patch|delete)|router\.(?:get|post)|@(?:Get|Post|Put|Delete)|def\s+\w+\(.*request)/i.test(c) &&
722
+ !looksLikeStarterBoilerplate(c)) {
723
+ return true;
724
+ }
725
+ return false;
726
+ }
727
+ function extractWriteContentsFromCall(call) {
728
+ const out = [];
729
+ if ((call.name === "fs.write" ||
730
+ call.name === "fs.append" ||
731
+ call.name === "fs.replaceLines") &&
732
+ typeof call.args.content === "string") {
733
+ out.push(call.args.content);
734
+ }
735
+ if (call.name === "fs.edit" && typeof call.args.newText === "string") {
736
+ out.push(call.args.newText);
737
+ }
738
+ if (call.name === "fs.writeMany" && Array.isArray(call.args.files)) {
739
+ for (const f of call.args.files) {
740
+ if (f &&
741
+ typeof f === "object" &&
742
+ typeof f.content === "string") {
743
+ out.push(f.content);
744
+ }
745
+ }
746
+ }
747
+ return out;
748
+ }
324
749
  /**
325
750
  * True when a successful tool call implements product feature code (not only scaffold).
751
+ * Starter boilerplate alone does not count.
326
752
  */
327
753
  export function isFeatureImplementationCall(call) {
328
754
  const paths = extractWritePathsFromCall(call);
329
- if (paths.some(isAppSourcePath))
755
+ const contents = extractWriteContentsFromCall(call);
756
+ if (contents.some(looksLikeFeatureProductCode))
757
+ return true;
758
+ // Content present but only starter → not feature
759
+ if (contents.length > 0 &&
760
+ contents.every((c) => looksLikeStarterBoilerplate(c) || c.trim().length < 40)) {
761
+ return false;
762
+ }
763
+ // Path-only signal: source write without inspectable content still counts
764
+ // (e.g. truncated args) but not lockfiles/config alone.
765
+ if (paths.some(isAppSourcePath) && contents.length === 0)
330
766
  return true;
331
- // Hand-written multi-file setup without official scaffolder
332
- if (call.name === "fs.writeMany" && paths.length >= 2)
767
+ if (paths.some(isAppSourcePath) &&
768
+ contents.some((c) => !looksLikeStarterBoilerplate(c) && c.trim().length >= 80)) {
333
769
  return true;
770
+ }
771
+ // Hand-written multi-file setup with non-starter content
772
+ if (call.name === "fs.writeMany" &&
773
+ paths.length >= 2 &&
774
+ contents.some((c) => !looksLikeStarterBoilerplate(c))) {
775
+ return true;
776
+ }
334
777
  return false;
335
778
  }
336
- /**
337
- * Soft block: don't start the local server until the product feature exists.
338
- * Returns a message when the open work is still scaffold-only for a feature ask.
339
- */
340
- export function incompleteFeatureBeforeServerMessage(userPrompt, sawFeatureImpl, call) {
341
- if (!userAskedForFeatureApp(userPrompt))
342
- return undefined;
343
- if (sawFeatureImpl)
344
- return undefined;
345
- if (!isDevServerCall(call) && !(call.name === "shell.start"))
346
- return undefined;
347
- return (`Feature incomplete: the user asked for a product app (not just a blank scaffold). ` +
348
- `Implement the requested feature first (real UI/state or API — replace starter boilerplate), ` +
349
- `THEN start the server. Do not shell.start / npm run dev until the feature is written.`);
350
- }
351
779
  /**
352
780
  * User said work belongs on the Desktop (or similar). Returns absolute path.
353
781
  */