carthagent 0.0.0-stage → 1.0.7

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 (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,610 @@
1
+ import { workflowD2, validateWorkflow } from './workflow.mjs';
2
+ import { outcomeContractErrors } from './outcome-contract.mjs';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { existsSync, lstatSync, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync, openSync, writeSync, closeSync } from 'node:fs';
5
+ import { resolve, relative, isAbsolute, join, dirname, basename } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ // Generic dependency/cache/output directories excluded from the source fingerprint.
9
+ // Scenarios or projects can add more via the manifest; these are the common defaults.
10
+ const OMIT = new Set(['.git', '.venv', 'venv', 'node_modules', '__pycache__', '.harness', 'artifacts', 'saves', '.tools', '.pytest_cache', '.ruff_cache', 'dist', 'build', '.next', '.nuxt', '.cache', 'coverage', '.turbo', 'target', '.pi', '.carthagent', '.yarn', '.pnpm-store', '.output', '.svelte-kit', '.docusaurus']);
11
+ export const evidenceExcludedDirectories = Object.freeze([...OMIT]);
12
+ const DEFAULT_MAX_FILES = 25000;
13
+ const DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
14
+
15
+ export function maxEvidenceFiles() {
16
+ const envVal = process.env.CARTHAGENT_MAX_FILES || process.env.PI_MAX_FILES;
17
+ if (envVal) {
18
+ const parsed = parseInt(envVal, 10);
19
+ if (!Number.isNaN(parsed) && parsed > 0) return parsed;
20
+ }
21
+ return DEFAULT_MAX_FILES;
22
+ }
23
+
24
+ export function maxEvidenceBytes() {
25
+ const envVal = process.env.CARTHAGENT_MAX_BYTES || process.env.PI_MAX_BYTES;
26
+ if (envVal) {
27
+ const parsed = parseInt(envVal, 10);
28
+ if (!Number.isNaN(parsed) && parsed > 0) return parsed;
29
+ }
30
+ return DEFAULT_MAX_BYTES;
31
+ }
32
+
33
+ export const VERIFICATION_MODES = ['required', 'advisory', 'none'];
34
+ export const DEFAULT_VERIFICATION = 'required';
35
+
36
+ /** Normalize a plan's verification classification. Unknown/missing -> 'required'. */
37
+ export function verificationMode(plan) {
38
+ const mode = plan?.verification ?? DEFAULT_VERIFICATION;
39
+ return VERIFICATION_MODES.includes(mode) ? mode : DEFAULT_VERIFICATION;
40
+ }
41
+
42
+ /**
43
+ * Cheap, pure read of the user's request text: does it read as a question or a
44
+ * read-only/explain ask rather than a build/fix task? Mirrors the framing gate
45
+ * in lib/tui/framing.mjs so the extension can hint at the right classification.
46
+ */
47
+ export function looksInformational(text) {
48
+ const v = String(text ?? '').trim();
49
+ if (!v) return false;
50
+ if (v.startsWith('/') || v.startsWith('!')) return false;
51
+ // Polite implementation requests are still delivery work, even with a '?'.
52
+ if (/\b(build|create|implement|write|add|fix|make|generate|refactor|migrate|remove|update|upgrade)\b/i.test(v)) return false;
53
+ if (v.endsWith('?') && v.length < 300) return true;
54
+ return /^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) &&
55
+ v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v);
56
+ }
57
+
58
+ export function localPath(cwd, path) {
59
+ if (typeof path !== 'string' || !path.trim()) throw Error('A nonempty relative path is required');
60
+ const full = resolve(cwd, path.replace(/^@/, ''));
61
+ const rel = relative(resolve(cwd), full);
62
+ if (rel.startsWith('..' + '/') || rel.startsWith('..' + '\\') || rel === '..' || isAbsolute(rel)) throw Error('Path escapes working directory');
63
+ let cursor = resolve(cwd);
64
+ for (const part of rel.split(/[\\/]/).filter(Boolean)) {
65
+ cursor = join(cursor, part);
66
+ if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) throw Error(`Symlink not supported: ${cursor}`);
67
+ }
68
+ return full;
69
+ }
70
+
71
+ export function fingerprint(cwd, roots) {
72
+ const files = new Set();
73
+ let bytes = 0;
74
+ const hash = createHash('sha256');
75
+ const maxFiles = maxEvidenceFiles();
76
+ const maxBytes = maxEvidenceBytes();
77
+ function visit(path) {
78
+ if (OMIT.has(basename(path)) || /\.(pyc|pyo|class|o|obj|exe|dll|so|dylib|jar|wasm)$/.test(path)) return;
79
+ if (!existsSync(path)) { hash.update(`missing:${relative(cwd, path)}\0`); return; }
80
+ const stat = lstatSync(path);
81
+ if (stat.isSymbolicLink()) throw Error(`Symlink not supported in evidence scope: ${path}`);
82
+ if (stat.isDirectory()) {
83
+ hash.update(`directory:${relative(cwd, path)}\0`);
84
+ for (const name of readdirSync(path).sort()) visit(join(path, name));
85
+ } else if (stat.isFile()) {
86
+ files.add(path);
87
+ if (files.size > maxFiles) throw Error(`Evidence scope exceeds ${maxFiles} files; use a narrower project cwd or move generated/dependency files to excluded directories`);
88
+ }
89
+ }
90
+ for (const root of [...roots].sort()) visit(localPath(cwd, root));
91
+ for (const file of [...files].sort()) {
92
+ bytes += lstatSync(file).size;
93
+ if (bytes > maxBytes) throw Error(`Evidence scope exceeds ${Math.round(maxBytes / (1024 * 1024))} MiB; use a narrower project cwd or move generated/dependency files to excluded directories`);
94
+ hash.update(relative(cwd, file)).update('\0').update(readFileSync(file)).update('\0');
95
+ }
96
+ return hash.digest('hex');
97
+ }
98
+
99
+ export function validatePlan(plan, cwd) {
100
+ const outcomeErrors = outcomeContractErrors(plan || {});
101
+ if (outcomeErrors.length) throw Error(outcomeErrors.join('; '));
102
+ if (!plan || typeof plan.goal !== 'string' || !plan.goal.trim()) throw Error('Goal is required');
103
+ if (!Array.isArray(plan.artifacts) || !plan.artifacts.length) throw Error('Declare source, test, configuration and documentation artifact roots');
104
+ plan.artifacts.forEach(p => {
105
+ const full = localPath(cwd, p);
106
+ const parts = relative(resolve(cwd), full).split(/[\\/]/);
107
+ if (parts.some(part => OMIT.has(part))) throw Error('Artifact roots must be product source/tests/config/docs, not ignored output or dependency directories. Use ["."] for the project.');
108
+ });
109
+ if (plan.outputs != null) {
110
+ if (!Array.isArray(plan.outputs)) throw Error('outputs must be an array of relative file paths');
111
+ // Outputs are produced evidence (screenshots, reports, fixtures) and may
112
+ // live under ignored dirs such as artifacts/ — localPath still blocks escapes.
113
+ plan.outputs.forEach(p => localPath(cwd, p));
114
+ }
115
+ const workflowErrors = validateWorkflow(plan);
116
+ if (workflowErrors.length) throw Error(workflowErrors.map(e => e.message).join('\n'));
117
+ if (!Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 40) throw Error('Use 1–40 implementation steps');
118
+ const steps = planSteps(plan);
119
+ const stepIds = new Set();
120
+ for (const step of steps) {
121
+ if (typeof step.title !== 'string' || !step.title.trim()) throw Error('Step titles must be nonempty strings');
122
+ if (typeof step.id !== 'string' || !/^[a-z][a-z0-9_-]{0,39}$/.test(step.id) || stepIds.has(step.id)) throw Error('Step IDs must be unique safe identifiers');
123
+ stepIds.add(step.id);
124
+ if (step.kind !== undefined && !['work', 'regression'].includes(step.kind)) throw Error('Step kind must be work or regression');
125
+ if (step.kind === 'regression' && step.checks.length) throw Error('Regression-capture steps must not require green checks. Use checks:[] here; retain passing suites on acceptance and the integration step. Capture a behavioral assertion failure, not an import/syntax error.');
126
+ for (const field of ['dependsOn', 'checks']) {
127
+ if (!Array.isArray(step[field]) || step[field].some(id => typeof id !== 'string') || new Set(step[field]).size !== step[field].length) throw Error(`Step ${field} must contain unique IDs`);
128
+ }
129
+ }
130
+ const visiting = new Set(), visited = new Set();
131
+ function visitStep(step) {
132
+ if (visiting.has(step.id)) throw Error('Step dependencies must not contain cycles');
133
+ if (visited.has(step.id)) return;
134
+ visiting.add(step.id);
135
+ for (const id of step.dependsOn) {
136
+ if (!stepIds.has(id)) throw Error(`Unknown step dependency: ${id}`);
137
+ visitStep(steps.find(s => s.id === id));
138
+ }
139
+ visiting.delete(step.id); visited.add(step.id);
140
+ }
141
+ steps.forEach(visitStep);
142
+ const verification = plan.verification ?? DEFAULT_VERIFICATION;
143
+ if (!VERIFICATION_MODES.includes(verification)) throw Error(`verification must be one of ${VERIFICATION_MODES.join(', ')} (default required)`);
144
+ const checks = plan.checks ?? [];
145
+ const acceptance = plan.acceptance ?? [];
146
+ if (!Array.isArray(checks) || !Array.isArray(acceptance)) throw Error('checks and acceptance must be arrays');
147
+ if (verification === 'required' && (!checks.length || !acceptance.length)) throw Error('Acceptance criteria and executable checks are required');
148
+ if (verification === 'none' && (checks.length || acceptance.length)) throw Error('verification:"none" is a pure informational ask and cannot declare checks or acceptance criteria; use "advisory" for optional evidence');
149
+ if (verification !== 'none' && (checks.length > 0) !== (acceptance.length > 0)) throw Error('Acceptance criteria and checks must be declared together');
150
+ const ids = new Set();
151
+ for (const check of checks) {
152
+ if (!check || typeof check.id !== 'string' || check.id === 'all' || !/^[a-z][a-z0-9_-]{0,39}$/.test(check.id) || ids.has(check.id)) throw Error('Check IDs must be unique safe identifiers');
153
+ ids.add(check.id);
154
+ if (!Array.isArray(check.argv) || !check.argv.length || check.argv.some(s => typeof s !== 'string' || !s.length)) throw Error('Check argv must contain an executable and separate arguments');
155
+ if (!['test', 'runtime', 'static'].includes(check.kind)) throw Error('Check kind must be test, runtime or static');
156
+ if (!Number.isInteger(check.timeoutSeconds) || check.timeoutSeconds < 1 || check.timeoutSeconds > 300) throw Error('Check timeout must be 1–300 seconds');
157
+ }
158
+ if (verification === 'required' && !checks.some(c => c.kind !== 'static')) throw Error('Syntax checks alone are insufficient: add a test or runtime check');
159
+ for (const step of steps) {
160
+ if (step.checks.some(id => !ids.has(id))) throw Error(`Step ${step.id} references unknown checks`);
161
+ }
162
+ const requirements = new Set();
163
+ for (const item of acceptance) {
164
+ if (requirements.has(item?.requirement)) throw Error('Acceptance requirement text must be unique');
165
+ requirements.add(item?.requirement);
166
+ if (!item || typeof item.requirement !== 'string' || !item.requirement.trim() || !Array.isArray(item.checks) || !item.checks.length || item.checks.some(id => !ids.has(id))) throw Error(`Every acceptance criterion must reference existing check IDs. Available: ${[...ids].join(', ')}. Multiple criteria can share a test suite.`);
167
+ }
168
+ return structuredClone(plan);
169
+ }
170
+
171
+ /** Legacy labels remain supported; structured steps add stable identity and gates. */
172
+ export function planSteps(plan) {
173
+ return (plan?.steps || []).map((step, i) => typeof step === 'string'
174
+ ? { id: `step${i}`, title: step, dependsOn: [], checks: [] }
175
+ : { ...step, dependsOn: step?.dependsOn ?? [], checks: step?.checks ?? [] });
176
+ }
177
+
178
+ export function validatePlanPatch(patch) {
179
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch) || !Object.keys(patch).length) throw Error('A nonempty plan patch is required');
180
+ const fields = new Set(['goal', 'assumptions', 'artifacts', 'outputs', 'steps', 'verification', 'acceptance', 'checks', 'design', 'workflow', 'outcomes']);
181
+ if (Object.keys(patch).some(key => !fields.has(key))) throw Error('Unknown plan patch field');
182
+ return patch;
183
+ }
184
+
185
+ export function mergePlanPatch(plan, patch) {
186
+ validatePlanPatch(patch);
187
+ const merged = { ...plan, ...patch };
188
+ for (const field of ['design', 'workflow']) {
189
+ if (patch[field] && typeof patch[field] === 'object' && !Array.isArray(patch[field])) merged[field] = { ...plan[field], ...patch[field] };
190
+ }
191
+ return merged;
192
+ }
193
+
194
+ /** Pure revision: no command executions, no fabricated passing evidence. */
195
+ export function revisePlan(previous, patch, { cwd, reason, hash } = {}) {
196
+ if (!previous?.plan) throw Error('Call delivery_plan first');
197
+ if (typeof reason !== 'string' || !reason.trim()) throw Error('A revision reason is required');
198
+ validatePlanPatch(patch);
199
+ const plan = validatePlan(mergePlanPatch(previous.plan, patch), cwd);
200
+ for (const outcome of previous.plan.outcomes || []) {
201
+ if (!plan.outcomes?.some(o => o.id === outcome.id && o.requirement === outcome.requirement)) throw Error(`Cannot drop outcome obligation ${outcome.id}; preserve it and record unavailable evidence as unverified.`);
202
+ }
203
+ // Resuming blocked/verified work is still the same contract, unlike a new task.
204
+ validateRevision({ ...previous, status: 'implementing' }, plan);
205
+ const next = structuredClone(previous);
206
+ next.plan = plan;
207
+ next.revision = (previous.revision || 1) + 1;
208
+ next.status = 'implementing';
209
+ next.reviewRequired = requiresReview(previous) || requiresReview(next);
210
+ delete next.handoff;
211
+ delete next.reviewEvidence;
212
+ delete next.reviewCapture;
213
+ if (next.planning) {
214
+ next.planning.history = [...(next.planning.history || []), { event: 'revised', revision: previous.revision, reason, approval: previous.planning?.approval || null }];
215
+ delete next.planning.approval;
216
+ delete next.planning.started;
217
+ }
218
+ // Preserve truthful command evidence, but newly added/remapped requirements
219
+ // must not inherit coverage from executions predating the requirement.
220
+ next.requirementRevisions = Object.create(null);
221
+ for (const item of plan.acceptance || []) {
222
+ const old = (previous.plan.acceptance || []).find(a => a.requirement === item.requirement);
223
+ const same = old && JSON.stringify([...old.checks].sort()) === JSON.stringify([...item.checks].sort());
224
+ next.requirementRevisions[item.requirement] = same ? requirementRevision(previous, item.requirement) : next.revision;
225
+ }
226
+ next.verificationStarted = Boolean(previous.verificationStarted || Object.keys(previous.evidence || {}).length);
227
+ next.evidence = {};
228
+ for (const check of plan.checks || []) {
229
+ const oldCheck = (previous.plan.checks || []).find(c => c.id === check.id);
230
+ const evidence = previous.evidence?.[check.id];
231
+ if (!oldCheck || checkDigest(oldCheck) !== checkDigest(check) || !evidence) continue;
232
+ next.evidence[check.id] = structuredClone(evidence);
233
+ // Rebind only genuinely fresh evidence. Stale/failed evidence stays visible,
234
+ // but its old identity can never become passing merely through replanning.
235
+ if (hash && evidenceFresh(oldCheck, evidence, hash, previous)) {
236
+ Object.assign(next.evidence[check.id], evidenceIdentity(next, check), {
237
+ executedRevision: evidence.executedRevision ?? evidence.originRevision ?? evidence.revision,
238
+ originRevision: evidence.originRevision ?? previous.revision,
239
+ });
240
+ }
241
+ }
242
+ const before = planSteps(previous.plan), after = planSteps(plan);
243
+ const signature = step => JSON.stringify([step.title, step.kind || 'work', [...step.dependsOn].sort(), [...step.checks].sort()]);
244
+ next.stepStatus = {};
245
+ after.forEach((step, i) => {
246
+ const legacy = typeof plan.steps[i] === 'string';
247
+ const candidates = before.map((s, index) => ({ s, index })).filter(({ s, index }) =>
248
+ legacy ? typeof previous.plan.steps[index] === 'string' && s.title === step.title : typeof previous.plan.steps[index] !== 'string' && s.id === step.id);
249
+ // Duplicate legacy titles are ambiguous: never guess which completed step moved.
250
+ if (candidates.length !== 1 || (legacy && after.filter(s => s.title === step.title).length !== 1)) return;
251
+ const { s, index } = candidates[0];
252
+ if (signature(s) !== signature(step)) return;
253
+ if (previous.workflowMode !== 'standard' && step.checks.some(id => pendingChecks(next, hash).includes(id))) return;
254
+ if (previous.stepStatus?.[`step${index}`]) next.stepStatus[`step${i}`] = previous.stepStatus[`step${index}`];
255
+ });
256
+ for (let pass = 0; pass < after.length; pass++) after.forEach((step, i) => {
257
+ if (step.dependsOn.some(id => next.stepStatus[`step${after.findIndex(s => s.id === id)}`] !== 'done')) delete next.stepStatus[`step${i}`];
258
+ });
259
+ next.revisions = [...(previous.revisions || []), {
260
+ revision: previous.revision, reason: reason.trim(), at: new Date().toISOString(),
261
+ plan: structuredClone(previous.plan), evidence: structuredClone(previous.evidence || {}),
262
+ stepStatus: structuredClone(previous.stepStatus || {}), status: previous.status,
263
+ requirementRevisions: structuredClone(previous.requirementRevisions || {}),
264
+ reviewEvidence: structuredClone(previous.reviewEvidence || null),
265
+ planning: structuredClone(previous.planning || null),
266
+ }];
267
+ return next;
268
+ }
269
+
270
+ function requirementRevision(state, requirement) {
271
+ const value = state.requirementRevisions;
272
+ return value && Object.hasOwn(value, requirement) ? value[requirement] : 1;
273
+ }
274
+
275
+ export function pendingRequirements(state, hash) {
276
+ return (state.plan.acceptance || []).filter(item => item.checks.some(id => {
277
+ const check = state.plan.checks.find(c => c.id === id), evidence = state.evidence?.[id];
278
+ return !check || !evidenceFresh(check, evidence, hash, state) ||
279
+ (evidence.executedRevision ?? evidence.originRevision ?? evidence.revision) < requirementRevision(state, item.requirement);
280
+ })).map(item => item.requirement);
281
+ }
282
+
283
+ export function requiresReview(state) {
284
+ return verificationMode(state.plan) === 'required' && Boolean(state.reviewRequired || state.plan.steps.some(s => typeof s !== 'string'));
285
+ }
286
+
287
+ export function reviewFresh(state, hash) {
288
+ const review = state.reviewEvidence;
289
+ return Boolean(!(state.reviewFindings || []).some(f => !f.resolution) && review && review.runId === state.runId && review.revision === state.revision && review.fingerprint === hash);
290
+ }
291
+
292
+ /** Readiness is shared by status and finish; a green test alone is not completion. */
293
+ export function completionIssues(state, cwd, hash) {
294
+ if (verificationMode(state.plan) !== 'required') return { pendingChecks: [], missingArtifacts: [], missingOutputs: [], incompleteSteps: [], unverifiedRequirements: [], review: [] };
295
+ return {
296
+ pendingChecks: pendingChecks(state, hash),
297
+ unverifiedRequirements: pendingRequirements(state, hash),
298
+ review: (state.reviewFindings || []).some(f => !f.resolution)
299
+ ? state.reviewFindings.filter(f => !f.resolution).map(f => `Unresolved review finding ${f.id}: ${f.description}`)
300
+ : requiresReview(state) && !reviewFresh(state, hash) ? ['Use delivery_review to inspect the final diff and record coverage, boundary probes and findings.'] : [],
301
+ missingArtifacts: state.plan.artifacts.filter(path => !existsSync(localPath(cwd, path))),
302
+ missingOutputs: (state.plan.outputs || []).filter(path => {
303
+ const full = localPath(cwd, path);
304
+ return !existsSync(full) || !lstatSync(full).isFile();
305
+ }),
306
+ incompleteSteps: planSteps(state.plan).filter((step, i) =>
307
+ typeof state.plan.steps[i] !== 'string'
308
+ ? state.stepStatus?.[`step${i}`] !== 'done'
309
+ : ['active', 'failed', 'pending'].includes(state.stepStatus?.[`step${i}`])
310
+ ).map(step => step.id),
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Update the status of a plan step. Returns the updated state.
316
+ * status: 'active' | 'done' | 'failed' | 'pending'
317
+ * Validates that the step index exists in the plan.
318
+ */
319
+ export function updateStepStatus(state, stepIndex, status, hash) {
320
+ if (!state?.plan?.steps) throw Error('No delivery plan');
321
+ const steps = planSteps(state.plan);
322
+ if (typeof stepIndex === 'string') stepIndex = steps.findIndex(s => s.id === stepIndex);
323
+ if (!Number.isInteger(stepIndex) || stepIndex < 0 || stepIndex >= state.plan.steps.length) {
324
+ throw Error(`Invalid step index ${stepIndex}; plan has ${state.plan.steps.length} steps (0–${state.plan.steps.length - 1})`);
325
+ }
326
+ if (!['active', 'done', 'failed', 'pending'].includes(status)) throw Error('status must be active, done, failed, or pending');
327
+ const step = steps[stepIndex];
328
+ if (['active', 'done'].includes(status)) {
329
+ const waiting = step.dependsOn.filter(id => state.stepStatus?.[`step${steps.findIndex(s => s.id === id)}`] !== 'done');
330
+ if (waiting.length) throw Error(`Step ${step.id} is waiting on dependencies: ${waiting.join(', ')}`);
331
+ }
332
+ if (status === 'done' && step.checks.length && state.workflowMode !== 'standard') {
333
+ const pending = new Set(pendingChecks(state, hash));
334
+ if (step.checks.some(id => pending.has(id))) throw Error(`Step ${step.id} requires fresh passing checks: ${step.checks.join(', ')}`);
335
+ }
336
+ if (!state.stepStatus) state.stepStatus = {};
337
+ state.stepStatus[`step${stepIndex}`] = status;
338
+ // Reopening a prerequisite invalidates downstream progress transitively.
339
+ const reset = new Set(status === 'done' ? [] : [step.id]);
340
+ for (let pass = 0; pass < steps.length; pass++) steps.forEach((s, i) => {
341
+ if (!reset.has(s.id) && s.dependsOn.some(id => reset.has(id))) {
342
+ reset.add(s.id); state.stepStatus[`step${i}`] = 'pending';
343
+ }
344
+ });
345
+ delete state.handoff;
346
+ state.status = 'implementing';
347
+ return state;
348
+ }
349
+
350
+ /**
351
+ * Regenerate plan.d2 source with current step statuses reflected in labels.
352
+ * Annotates each step label with [active]/[done]/[failed]/[pending] markers.
353
+ */
354
+ export function planD2WithProgress(plan, stepStatus = {}) {
355
+ if (plan.workflow) return workflowD2(plan, stepStatus);
356
+ const lines = ['direction: down', `goal: ${JSON.stringify(plan.goal)}`];
357
+ const steps = planSteps(plan);
358
+ steps.forEach((s, i) => {
359
+ const st = stepStatus[`step${i}`] || 'pending';
360
+ const marker = st === 'done' ? ' ✓' : st === 'failed' ? ' ✗' : st === 'active' ? ' ▶' : '';
361
+ lines.push(`step${i}: ${JSON.stringify(s.title + marker)}`);
362
+ const dependencies = typeof plan.steps[i] === 'string' ? (i ? [steps[i - 1].id] : []) : s.dependsOn;
363
+ for (const id of dependencies) lines.push(`step${steps.findIndex(s => s.id === id)} -> step${i}`);
364
+ if (!dependencies.length) lines.push(`goal -> step${i}`);
365
+ });
366
+ lines.push('verify: "Verify checks"');
367
+ steps.forEach((s, i) => {
368
+ if (!steps.some(other => other.dependsOn.includes(s.id)) && (typeof plan.steps[i] !== 'string' || i === steps.length - 1)) lines.push(`step${i} -> verify`);
369
+ });
370
+ lines.push('review: "Review + limits"', 'verify -> review', 'repair: "Repair failures"', 'verify -> repair: failure', 'repair -> verify', 'deliver: "Deliver handoff"', 'review -> deliver');
371
+ return lines.join('\n') + '\n';
372
+ }
373
+
374
+ export function bindRequiredChecks(plan, required) {
375
+ const bound = structuredClone(plan);
376
+ const ids = new Set(required.map(check => check.id));
377
+ // User-owned validators are non-negotiable: force strict verification so a
378
+ // plan cannot declare 'advisory'/'none' to route around them.
379
+ if (required.length) bound.verification = 'required';
380
+ bound.checks = [...(bound.checks || []).filter(check => !ids.has(check.id)), ...structuredClone(required)];
381
+ bound.acceptance = (bound.acceptance || []).filter(item => !item.requirement.startsWith('[Harness required] '));
382
+ for (const check of required) bound.acceptance.push({ requirement: `[Harness required] ${check.id}`, checks: [check.id] });
383
+ return bound;
384
+ }
385
+
386
+ export function loadRequiredChecks(path, cwd) {
387
+ const data = JSON.parse(readFileSync(path, 'utf8'));
388
+ if (data.version !== 1 || !Array.isArray(data.checks) || !data.checks.length || data.checks.length > 12) throw Error('Validator manifest requires version:1 and 1–12 checks');
389
+ if (data.checks.some(check => typeof check.id !== 'string' || check.id.length > 31)) throw Error('Required validator IDs must be at most 31 characters (room for required_ prefix)');
390
+ // Validate using the same command schema as model-declared checks.
391
+ validatePlan({ goal: 'Required verification', artifacts: ['.'], steps: ['Verify'], acceptance: [{ requirement: 'External checks', checks: data.checks.map(c => c.id) }], checks: data.checks }, cwd);
392
+ return data.checks.map(check => ({ ...check, id: `required_${check.id}` }));
393
+ }
394
+
395
+ export function validateRevision(previous, next) {
396
+ if (!previous || ['verified', 'blocked'].includes(previous.status)) return;
397
+ const before = verificationMode(previous.plan);
398
+ const after = verificationMode(next);
399
+ const hasEvidence = previous.verificationStarted || Object.keys(previous.evidence || {}).length > 0;
400
+ // Pre-verify re-check: an ask can be reclassified from delivery to
401
+ // informational/advisory only before any check has run. Once real evidence
402
+ // exists, downgrading would let a failing contract escape its checks.
403
+ if (before === 'required' && after !== 'required') {
404
+ if (hasEvidence) throw Error('Cannot downgrade verification after checks have run. Keep the requirement, or report status=blocked with the failing evidence.');
405
+ return;
406
+ }
407
+ const outputs = new Set(next.outputs || []);
408
+ if ((previous.plan.outputs || []).some(path => !outputs.has(path))) throw Error('Replanning cannot silently remove promised outputs; retain them or report the task blocked.');
409
+ const requirements = new Set((next.acceptance || []).map(item => item.requirement));
410
+ const dropped = (previous.plan.acceptance || []).filter(item => !requirements.has(item.requirement));
411
+ if (dropped.length) throw Error('Replanning cannot silently remove acceptance criteria. Retain the original requirement text and adjust check commands, or report the task blocked.');
412
+ }
413
+
414
+ export function planD2(plan) {
415
+ return planD2WithProgress(plan);
416
+ }
417
+
418
+ export function atomicJson(path, data) {
419
+ mkdirSync(dirname(path), { recursive: true });
420
+ const temp = `${path}.${randomUUID()}.tmp`;
421
+ writeFileSync(temp, JSON.stringify(data, null, 2) + '\n');
422
+ renameSync(temp, path);
423
+ }
424
+
425
+ export function killTree(child) {
426
+ if (!child.pid) return;
427
+ if (process.platform === 'win32') {
428
+ const killer = spawn('taskkill.exe', ['/pid', String(child.pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
429
+ killer.on('error', () => child.kill());
430
+ } else {
431
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
432
+ }
433
+ }
434
+
435
+ // No implicit shell. Use ["bash", "-lc", "..."] explicitly when a shell is necessary.
436
+ export async function runCommand(argv, { cwd, timeoutSeconds = 60, signal, logPath, onOutput } = {}) {
437
+ if (signal?.aborted) return { code: null, cancelled: true, timedOut: false, output: 'Cancelled before launch', durationMs: 0 };
438
+ let fd = null;
439
+ if (logPath) {
440
+ mkdirSync(dirname(logPath), { recursive: true });
441
+ fd = openSync(logPath, 'w');
442
+ }
443
+ const started = Date.now();
444
+ return new Promise(resolveResult => {
445
+ let tail = '', written = 0, timedOut = false, cancelled = false, outputLimit = false;
446
+ const child = spawn(argv[0], argv.slice(1), { cwd, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
447
+ const stop = () => killTree(child);
448
+ const abort = () => { cancelled = true; stop(); };
449
+ const timer = setTimeout(() => { timedOut = true; stop(); }, timeoutSeconds * 1000);
450
+ signal?.addEventListener('abort', abort, { once: true });
451
+ function consume(chunk) {
452
+ const text = chunk.toString();
453
+ tail = (tail + text).slice(-12000);
454
+ onOutput?.(text);
455
+ if (written + chunk.length <= 8 * 1024 * 1024) { if (fd !== null) writeSync(fd, chunk); written += chunk.length; }
456
+ else if (!outputLimit) { outputLimit = true; if (fd !== null) writeSync(fd, '\n[8 MiB output limit exceeded; terminated]\n'); stop(); }
457
+ }
458
+ child.stdout.on('data', consume);
459
+ child.stderr.on('data', consume);
460
+ child.on('error', e => consume(Buffer.from(String(e))));
461
+ child.on('close', code => {
462
+ clearTimeout(timer);
463
+ signal?.removeEventListener('abort', abort);
464
+ if (fd !== null) closeSync(fd);
465
+ resolveResult({ code, timedOut, cancelled, outputLimit, output: tail, logPath, durationMs: Date.now() - started });
466
+ });
467
+ if (signal?.aborted) abort();
468
+ });
469
+ }
470
+
471
+ // Tools in the same assistant message execute concurrently in Pi. Queue delivery
472
+ // operations rather than burning model turns rejecting sibling checks.
473
+ export function createSerialQueue() {
474
+ let tail = Promise.resolve();
475
+ return function enqueue(job) {
476
+ const result = tail.then(job);
477
+ tail = result.catch(() => {});
478
+ return result;
479
+ };
480
+ }
481
+
482
+ // Explicit field order makes identity independent of object property order.
483
+ export function checkDigest(check) {
484
+ return createHash('sha256').update(JSON.stringify([
485
+ check.id, check.kind, check.argv, check.timeoutSeconds
486
+ ])).digest('hex');
487
+ }
488
+
489
+ export function evidenceIdentity(state, check) {
490
+ return { runId: state.runId, revision: state.revision, checkDigest: checkDigest(check) };
491
+ }
492
+
493
+ export function evidenceFresh(check, evidence, hash, state) {
494
+ return Boolean(state?.runId && Number.isInteger(state.revision) && state.revision > 0 &&
495
+ evidence?.runId === state.runId && evidence.revision === state.revision &&
496
+ evidence.passed && evidence.fingerprint === hash &&
497
+ evidence.checkDigest === checkDigest(check));
498
+ }
499
+
500
+ export function pendingChecks(state, hash) {
501
+ return (state.plan.checks || []).filter(c => !evidenceFresh(c, state.evidence?.[c.id], hash, state)).map(c => c.id);
502
+ }
503
+
504
+ /**
505
+ * Per-acceptance-criterion evidence coverage.
506
+ * `fresh` counts checks that pass on the current workspace hash; `staticOnly`
507
+ * flags a criterion whose only fresh evidence is kind:"static" (syntax/import
508
+ * probes). The plan's self-declared `kind` is not proof of strength, so this is
509
+ * a signal for the rail and the handoff — not a hard gate.
510
+ */
511
+ export function coverageReport(plan, evidence = {}, hash = '', state = null) {
512
+ const byId = new Map((plan?.checks || []).map(c => [c.id, c]));
513
+ const unverified = new Set(state ? pendingRequirements(state, hash) : []);
514
+ return (plan?.acceptance || []).map(item => {
515
+ let fresh = 0, failing = 0, missing = 0, stale = 0;
516
+ const freshKinds = new Set();
517
+ for (const id of item.checks || []) {
518
+ const c = byId.get(id);
519
+ const e = evidence[id];
520
+ if (!e) { missing++; continue; }
521
+ if (!e.passed) { failing++; continue; }
522
+ if (!c || !evidenceFresh(c, e, hash, state)) { stale++; continue; }
523
+ fresh++;
524
+ if (c) freshKinds.add(c.kind);
525
+ }
526
+ const kinds = [...freshKinds];
527
+ return {
528
+ requirement: item.requirement,
529
+ checks: item.checks || [],
530
+ fresh, failing, missing, stale,
531
+ covered: fresh > 0 && failing === 0 && missing === 0 && stale === 0 && !unverified.has(item.requirement),
532
+ staticOnly: fresh > 0 && kinds.every(k => k === 'static'),
533
+ };
534
+ });
535
+ }
536
+
537
+ /** Fresh (current-hash, passing) check count for the rail header. */
538
+ export function freshChecks(state, hash) {
539
+ if (!state?.plan?.checks?.length) return { fresh: 0, total: 0 };
540
+ let fresh = 0;
541
+ for (const c of state.plan.checks) {
542
+ const e = state.evidence?.[c.id];
543
+ if (evidenceFresh(c, e, hash, state)) fresh++;
544
+ }
545
+ return { fresh, total: state.plan.checks.length };
546
+ }
547
+
548
+ export const PHASES = ['inspect', 'plan', 'build', 'verify', 'review', 'deliver'];
549
+
550
+ /**
551
+ * Derive the current delivery phase for the rail tracker.
552
+ * Pure: { delivery, hash, checkRunning } -> { phase, index, revision }.
553
+ * - no plan yet -> inspect (index 0)
554
+ * - verified / blocked -> deliver (index 5)
555
+ * - check tool currently running -> verify (index 3)
556
+ * - all checks fresh -> review (index 4)
557
+ *
558
+ * `status="verifying"` is persisted while a check runs, but it is not proof
559
+ * that a check is still active. A process can stop between report writes, so
560
+ * presentation must prefer current evidence and the live `checkRunning` signal.
561
+ * - pending/failed/stale checks or active/failed step -> build (index 2)
562
+ * - plan exists, no progress yet -> plan (index 1)
563
+ */
564
+ export function computePhase(delivery, hash, checkRunning = false) {
565
+ const revision = delivery?.revision || 1;
566
+ if (!delivery?.plan) return { phase: 'inspect', index: 0, revision };
567
+ const status = delivery.status;
568
+ if (status === 'verified' || status === 'blocked') return { phase: 'deliver', index: 5, revision };
569
+ if (checkRunning) return { phase: 'verify', index: 3, revision };
570
+ const pending = pendingChecks(delivery, hash);
571
+ const { fresh, total } = freshChecks(delivery, hash);
572
+ const unfinished = planSteps(delivery.plan).some((_, i) => typeof delivery.plan.steps[i] !== 'string' && delivery.stepStatus?.[`step${i}`] !== 'done');
573
+ if (unfinished) return { phase: 'build', index: 2, revision };
574
+ if (!pending.length && pendingRequirements(delivery, hash).length) return { phase: 'build', index: 2, revision };
575
+ if (total > 0 && pending.length === 0 && fresh === total) return { phase: 'review', index: 4, revision };
576
+ // Informational/advisory plans have nothing to verify: never park in build
577
+ // waiting for checks that are intentionally absent.
578
+ if (verificationMode(delivery.plan) !== 'required' && pending.length === 0) return { phase: 'review', index: 4, revision };
579
+ const stepStatus = delivery.stepStatus || {};
580
+ const stepped = Object.values(stepStatus).some(s => ['active', 'done', 'failed'].includes(s));
581
+ const hasEvidence = Object.keys(delivery.evidence || {}).length > 0;
582
+ // Fresh plan, nothing attempted yet -> still planning, not building.
583
+ if (pending.length && (hasEvidence || stepped)) return { phase: 'build', index: 2, revision };
584
+ if (pending.length) return { phase: 'plan', index: 1, revision };
585
+ if (stepped) return { phase: 'build', index: 2, revision };
586
+ return { phase: 'plan', index: 1, revision };
587
+ }
588
+
589
+ export function restoreState(entries) {
590
+ let state = null;
591
+ for (const entry of entries) {
592
+ if (entry.type === 'custom' && entry.customType === 'delivery-state-v1') state = structuredClone(entry.data);
593
+ }
594
+ return state;
595
+ }
596
+
597
+ export function shouldContinue({ state, touched, nudges, stopReason, pendingMessages, fresh }) {
598
+ // Informational/advisory plans are terminal once the agent stops: there is no
599
+ // verification contract to repair, so never nudge them into the repair loop.
600
+ if (state && verificationMode(state.plan) !== 'required') return false;
601
+ return nudges < 2 && !pendingMessages && stopReason === 'stop' &&
602
+ (state ? state.status !== 'blocked' && (state.status !== 'verified' || !fresh) : touched);
603
+ }
604
+
605
+ // Live-trial budget policy: provider/connection failures produce no product work
606
+ // and must not consume the productive model-turn budget; wall-clock still bounds the run.
607
+ export function turnBudgetExceeded({ turns, providerErrors, maxTurns }) {
608
+ if (![turns, providerErrors, maxTurns].every(Number.isInteger) || providerErrors < 0 || maxTurns < 0) throw Error('turns, providerErrors and maxTurns must be nonnegative integers');
609
+ return turns - providerErrors > maxTurns;
610
+ }