@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,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
  }
@@ -170,67 +540,18 @@ export function looksLikeScaffoldTaskTitle(title) {
170
540
  export function isPackageInstallCommand(cmd) {
171
541
  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
542
  }
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
543
  /**
221
544
  * Choose which pending plan task should own this tool call (auto-start).
222
545
  * Prevents npm install from being attributed to "localStorage" / style tasks.
223
546
  */
224
- export function pickPendingTaskForToolCall(pending, call, allTitles) {
547
+ export function pickPendingTaskForToolCall(pending, call, _allTitles) {
225
548
  if (pending.length === 0)
226
549
  return undefined;
227
- const titles = allTitles ?? pending.map((p) => p.title);
228
550
  const cmd = commandOf(call);
229
551
  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
552
+ // Heuristics rank likely ownership only. They must never make a pending
553
+ // task ineligible or authorize/block the underlying tool call.
554
+ let s = 1;
234
555
  if (isPackageInstallCommand(cmd)) {
235
556
  if (looksLikeInstallTaskTitle(title))
236
557
  s += 20;
@@ -255,6 +576,19 @@ export function pickPendingTaskForToolCall(pending, call, allTitles) {
255
576
  s += 12;
256
577
  }
257
578
  }
579
+ // Keyword overlap (SSRF task ↔ /api/og-image fetch, fuzz ↔ /api/*, …)
580
+ const blob = `${call.name} ${cmd} ${JSON.stringify(call.args ?? {})}`.toLowerCase();
581
+ const words = title
582
+ .toLowerCase()
583
+ .split(/[^a-z0-9]+/)
584
+ .filter((w) => w.length > 3);
585
+ let hits = 0;
586
+ for (const w of words) {
587
+ if (blob.includes(w))
588
+ hits += 1;
589
+ }
590
+ if (hits > 0)
591
+ s += Math.min(12, hits * 3);
258
592
  return s;
259
593
  };
260
594
  let best;
@@ -266,9 +600,7 @@ export function pickPendingTaskForToolCall(pending, call, allTitles) {
266
600
  bestScore = sc;
267
601
  }
268
602
  }
269
- if (!best || bestScore < 0)
270
- return undefined;
271
- return best;
603
+ return best ?? pending[0];
272
604
  }
273
605
  /**
274
606
  * User asked for a product feature app (todo, blog, …), not "just scaffold framework X".
@@ -321,33 +653,90 @@ export function extractWritePathsFromCall(call) {
321
653
  }
322
654
  return paths;
323
655
  }
656
+ /** Default framework starter / marketing copy — not the product feature. */
657
+ export function looksLikeStarterBoilerplate(content) {
658
+ const c = content.slice(0, 8_000);
659
+ return (/\bWelcome to (?:Vite|React|Next\.js|Create React App|Nuxt|SvelteKit|Angular)\b/i.test(c) ||
660
+ /\bGet started by editing\b/i.test(c) ||
661
+ /\bEdit\s+`?(?:src\/)?App\.(?:tsx?|jsx?)`?\s+and save to (?:test|reload)/i.test(c) ||
662
+ /\blogos?\/(?:react|vite|next)\.svg\b/i.test(c) ||
663
+ /\bnext\.js logo\b/i.test(c) ||
664
+ /https?:\/\/(?:react\.dev|vitejs\.dev|nextjs\.org)\/(?:learn|docs)?/i.test(c) && /\b(?:Deploy|Learn|Templates|create-next-app)\b/i.test(c));
665
+ }
666
+ /** Content that looks like real product UI/logic for common feature apps. */
667
+ export function looksLikeFeatureProductCode(content) {
668
+ const c = content.slice(0, 12_000);
669
+ if (looksLikeStarterBoilerplate(c))
670
+ return false;
671
+ // Interactive product signals (todo/crud/auth/etc.)
672
+ if (/\b(?:useState|useReducer|createContext|signal\(|createSignal|ref\()\b/.test(c) &&
673
+ /\b(?:todo|todos|onClick|onSubmit|addTodo|toggle|complete|delete|remove|login|signup|password|fetch\(|axios|localStorage)\b/i.test(c)) {
674
+ return true;
675
+ }
676
+ if (/\b(?:add|create|toggle|delete|remove|update)\b/i.test(c) &&
677
+ /\b(?:todo|item|task|post|note|user|cart)\b/i.test(c) &&
678
+ /\b(?:map\s*\(|filter\s*\(|set[A-Z]\w+|useState)\b/.test(c)) {
679
+ return true;
680
+ }
681
+ // API/backend feature
682
+ if (/\b(?:app\.(?:get|post|put|patch|delete)|router\.(?:get|post)|@(?:Get|Post|Put|Delete)|def\s+\w+\(.*request)/i.test(c) &&
683
+ !looksLikeStarterBoilerplate(c)) {
684
+ return true;
685
+ }
686
+ return false;
687
+ }
688
+ function extractWriteContentsFromCall(call) {
689
+ const out = [];
690
+ if ((call.name === "fs.write" ||
691
+ call.name === "fs.append" ||
692
+ call.name === "fs.replaceLines") &&
693
+ typeof call.args.content === "string") {
694
+ out.push(call.args.content);
695
+ }
696
+ if (call.name === "fs.edit" && typeof call.args.newText === "string") {
697
+ out.push(call.args.newText);
698
+ }
699
+ if (call.name === "fs.writeMany" && Array.isArray(call.args.files)) {
700
+ for (const f of call.args.files) {
701
+ if (f &&
702
+ typeof f === "object" &&
703
+ typeof f.content === "string") {
704
+ out.push(f.content);
705
+ }
706
+ }
707
+ }
708
+ return out;
709
+ }
324
710
  /**
325
711
  * True when a successful tool call implements product feature code (not only scaffold).
712
+ * Starter boilerplate alone does not count.
326
713
  */
327
714
  export function isFeatureImplementationCall(call) {
328
715
  const paths = extractWritePathsFromCall(call);
329
- if (paths.some(isAppSourcePath))
716
+ const contents = extractWriteContentsFromCall(call);
717
+ if (contents.some(looksLikeFeatureProductCode))
330
718
  return true;
331
- // Hand-written multi-file setup without official scaffolder
332
- if (call.name === "fs.writeMany" && paths.length >= 2)
719
+ // Content present but only starter → not feature
720
+ if (contents.length > 0 &&
721
+ contents.every((c) => looksLikeStarterBoilerplate(c) || c.trim().length < 40)) {
722
+ return false;
723
+ }
724
+ // Path-only signal: source write without inspectable content still counts
725
+ // (e.g. truncated args) but not lockfiles/config alone.
726
+ if (paths.some(isAppSourcePath) && contents.length === 0)
333
727
  return true;
728
+ if (paths.some(isAppSourcePath) &&
729
+ contents.some((c) => !looksLikeStarterBoilerplate(c) && c.trim().length >= 80)) {
730
+ return true;
731
+ }
732
+ // Hand-written multi-file setup with non-starter content
733
+ if (call.name === "fs.writeMany" &&
734
+ paths.length >= 2 &&
735
+ contents.some((c) => !looksLikeStarterBoilerplate(c))) {
736
+ return true;
737
+ }
334
738
  return false;
335
739
  }
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
740
  /**
352
741
  * User said work belongs on the Desktop (or similar). Returns absolute path.
353
742
  */