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,819 @@
1
+ import { validatePlanning, inspectPlanning, recordPlanReview, approvePlanning, planningStatus, startImplementation } from '../lib/planning.mjs';
2
+ import { needsImplementation } from '../lib/planning-access.mjs';
3
+ import { deliveryRecovery, designReceipt, archivalGroups, replaceArchivedGroups, contextMetrics, jsonBytes } from '../lib/delivery-context.mjs';
4
+ import { archiveContext } from '../lib/context-archive.mjs';
5
+ import { DELIVERY_INVARIANTS, TEST_INFRASTRUCTURE_GUIDANCE, deliveryPhaseGuidance } from '../lib/delivery-guidance.mjs';
6
+ import { probeDiagnostics } from '../lib/probe-diagnostics.mjs';
7
+ import { mergePlanPatch } from '../lib/delivery.mjs';
8
+ import { recoveryMessages } from '../lib/delivery-context.mjs';
9
+ import { configuredAdvisor, inspectAdvice } from '../lib/planning-advice.mjs';
10
+ import { createShipAdvisor } from '../lib/ship-jev.mjs';
11
+ import { CARTHAGENT_SHIP_PROVIDER_ID } from '../lib/providers/names.mjs';
12
+ import { PLANNING_START, planningGuide, planningNext } from '../lib/planning-guide.mjs';
13
+ import { newDiscovery, restoreDiscovery, discoveryEvent, discoveryCheckpoint } from '../lib/planning-discovery.mjs';
14
+ import { budgetLimits, newBudget, budgetReason, budgetSnapshot } from '../lib/budget.mjs';
15
+ import { CONFIG_DIR_NAME, truncateTail, type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
16
+ import { mkdirSync, writeFileSync, appendFileSync, existsSync, readFileSync, readdirSync } from 'node:fs';
17
+ import { basename, dirname, join, resolve } from 'node:path';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { lockWorkspace, selectReport, assertActiveReport } from '../lib/workspace.mjs';
21
+ import { saveReport, reconcileReport } from '../lib/reports.mjs';
22
+ import { evidenceIdentity, validatePlan, planD2WithProgress, fingerprint, runCommand, pendingChecks, restoreState, shouldContinue, localPath, createSerialQueue, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, completionIssues, planSteps, validatePlanPatch, pendingRequirements } from '../lib/delivery.mjs';
23
+ import { captureDeliveryReview, recordDeliveryReview } from '../lib/delivery-review.mjs';
24
+ import { formatGuidance, loadGuidanceProfiles, routeGuidance } from '../lib/guidance.mjs';
25
+
26
+ const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
27
+ const BROWSER_CHECK = join(EXTENSION_DIR, '..', 'tools', 'browser-check.mjs');
28
+ const BROWSER_CHECK_GUIDANCE = existsSync(BROWSER_CHECK)
29
+ ? `\nFor web/browser tasks, a self-contained browser check is available: ${JSON.stringify(BROWSER_CHECK)}. Declare it as a runtime check, e.g. ["node","${BROWSER_CHECK}","--page","index.html","--assert","#app","--assert-count","#items:3","--click","#btn","--then-text","#out:done","--console-clean","--screenshot","artifacts/ui.png"]. It serves the workspace over HTTP, runs jsdom assertions with real inline-script execution (clicks, text, selectors, console-error detection), and captures a real screenshot with the bundled lightweight headless browser (auto-downloaded on first use) — no local browser install needed.`
30
+ : '';
31
+
32
+ const text = (value: unknown) => {
33
+ const output = truncateTail(typeof value === 'string' ? value : JSON.stringify(value, null, 2), { maxBytes: 48000, maxLines: 1000 });
34
+ return { content: [{ type: 'text' as const, text: output.content + (output.truncated ? '\n[Output truncated; full check output is in the recorded log files.]' : '') }], details: {} };
35
+ };
36
+ type JsonSchema = Record<string, unknown>;
37
+ const optionalSchemas = new WeakSet<JsonSchema>();
38
+ const Type = {
39
+ String: (options: JsonSchema = {}): JsonSchema => ({ type: 'string', ...options }),
40
+ Integer: (options: JsonSchema = {}): JsonSchema => ({ type: 'integer', ...options }),
41
+ Union: (schemas: JsonSchema[]): JsonSchema => ({ anyOf: schemas }),
42
+ Partial: (schema: JsonSchema): JsonSchema => { const copy = { ...schema }; delete copy.required; return copy; },
43
+ Array: (items: JsonSchema, options: JsonSchema = {}): JsonSchema => ({ type: 'array', items, ...options }),
44
+ Optional: (schema: JsonSchema): JsonSchema => { optionalSchemas.add(schema); return schema; },
45
+ Object: (properties: Record<string, JsonSchema>, options: JsonSchema = {}): JsonSchema => {
46
+ const required = Object.entries(properties).filter(([, schema]) => !optionalSchemas.has(schema)).map(([name]) => name);
47
+ return { type: 'object', ...(required.length ? { required } : {}), properties, ...options };
48
+ },
49
+ };
50
+ const shortString = () => Type.String({ minLength: 1, maxLength: 1200 });
51
+ const strings = (maxItems = 20) => Type.Array(shortString(), { minItems: 1, maxItems });
52
+ const planFields = {
53
+ outcomes: Type.Optional(Type.Array(Type.Object({ id: shortString(), requirement: shortString(), entrypoint: shortString(), observable: shortString(), substitutions: Type.Array(shortString()), missedFailure: shortString(), checks: Type.Array(shortString()) }), { maxItems: 32, description: 'Experimental opt-in outcome contracts. Choose critical user paths. Exercise the documented normal entrypoint in a thin working slice early, then recheck after implementation. State observable success, all substitutions and a plausible defect component tests miss. Never substitute an alternate smoke/test mode for normal launch.' })),
54
+ design: Type.Optional({ type: 'object', description: 'Tested architecture: components, ports, scenarios, risks. Call delivery_design action=guide for exact fields and example.', additionalProperties: true }),
55
+ workflow: Type.Optional({ type: 'object', additionalProperties: true }),
56
+ goal: shortString(), assumptions: Type.Array(shortString(), { maxItems: 20 }),
57
+ artifacts: strings(40), outputs: Type.Optional(Type.Array(shortString(), { maxItems: 40 })),
58
+ steps: Type.Array(Type.Union([shortString(), Type.Object({
59
+ id: Type.String({ pattern: '^[a-z][a-z0-9_-]{0,39}$' }), title: shortString(),
60
+ kind: Type.Optional(Type.String({ description: 'work (default) or regression; regression captures intentional behavioral failure and must have checks:[]' })),
61
+ dependsOn: Type.Optional(Type.Array(shortString(), { maxItems: 40 })),
62
+ checks: Type.Optional(Type.Array(shortString(), { maxItems: 24 })),
63
+ })]), { minItems: 1, maxItems: 40 }),
64
+ verification: Type.Optional(Type.String({ description: 'required (default) | advisory | none' })),
65
+ acceptance: Type.Array(Type.Object({ requirement: shortString(), checks: strings(24) }), { maxItems: 40 }),
66
+ checks: Type.Array(Type.Object({ id: Type.String({ pattern: '^[a-z][a-z0-9_-]{0,39}$' }), kind: Type.String({ description: 'test, runtime, or static' }), argv: strings(80), timeoutSeconds: Type.Integer({ minimum: 1, maximum: 300 }) }), { maxItems: 24 }),
67
+ };
68
+ export const LEGACY_GUIDANCE = `Software delivery workflow (not required for questions or read-only reviews):
69
+ Integration discipline for substantial changes: before implementing, choose ONE canonical representation for shared data (units, ordering, axes, encoding, identity) and state explicit conversions at component boundaries. Avoid parallel hand-written representations that drift. Reuse the same conversion functions for producers and consumers. Verify installed library conventions with executable probes rather than intuition. After the first runnable slice, assert a known asymmetric sample through the ACTUAL adapter/entrypoint and compare its observed result with the specification, not with another implementation-derived value. Then exercise each user-visible operation independently, including inverse actions, altered context/direction, and invalid or overlapping state where relevant. A boolean such as moved=true or nonzero output proves activity, not correctness: assert the requested effect, correspondence and safety invariants. At final review, identify the most consequential defect that could pass all current tests; write and execute a falsification probe of that defect. Use the actual resulting state/side effect rather than source-text assertions or successful process exit. Repair and rerun checks. If these probes cannot be executed, retain the affected behavior as unverified rather than claiming completion. This is task-agnostic integration reasoning, not a requirement to test every graph edge or create a separate test per criterion.
70
+ Classify the ask before planning. Pure questions, explanations and read-only reviews are informational: either answer directly without delivery_plan, or — when a written contract helps — call delivery_plan with verification:"none" (no checks) or verification:"advisory" (legacy optional evidence, not permission to execute commands). Reserve verification:"required" (the default) for tasks that change product files. Before running delivery_check, re-check the classification once more: a required plan may be reclassified to advisory/none only before any check has run, so decide before the verify loop. Informational plans finish cleanly with delivery_finish status="verified" and receive no repair nudges.
71
+ For substantial implementation work, inspect relevant entry points and installed library APIs first; do not wait for exhaustive discovery before proposing a concrete plan. Identify which domain skills or knowledge bases apply to this task — check the available skills list and read the matching skill before implementing. Use delivery_plan BEFORE implementation: capture assumptions, a small vertical-slice plan, artifact roots, acceptance criteria and real check commands. D2 is generated for the flowchart; use D2 for any additional flowcharts.
72
+ Before any code, tests or commands, declare design:{components,ports,scenarios,risks}; use delivery_design action=guide for the compact contract (docs/tested-planning.md has extended details). Use delivery_design action=validate, action=inspect to read the captured plan and its result.id, action=review with review:{captureId:<that id>, adversarial challenges and every scenario walkthrough}, then action=approve. Blocking findings persist until a later design revision supplies resolutions:[{id,change,evidence}]; revalidate/inspect after revising. Shell/unknown tools and check execution remain locked until approval. Discovery uses read/ls/find/grep, not executable probes. Every plan revision relocks implementation. Review is model-authored, not independent proof. Confirm proposed assertions can actually detect failure. If the plan is weak or incomplete, call delivery_revise with a reason and a partial plan patch to fix it — the plan is a living contract, not a one-time artifact. Unchanged checks retain evidence only while the workspace fingerprint is unchanged; source edits still require re-verification.
73
+ Implement a runnable slice early, then complete the agreed behavior in small coherent steps. Mark progress with delivery_progress as each step finishes so the plan panel stays current. Don't stop at a scaffold. Verify uncertain APIs with installed source or a tiny executable probe; never invent library methods or assume assets exist. Separate testable logic from rendering/services. Include error handling, dependencies, launch instructions, and regression tests. Exercise actual interaction paths in fresh subprocesses with the normal environment, not only compilation or internal function calls. Include non-ASCII text, paths with spaces, and invalid data where applicable. On Windows, stdout may use a legacy code page (e.g. cp1252): use ASCII-escaped JSON or configure the application's UTF-8 output; don't hide failures by changing only the test environment. For visual work, capture and inspect a screenshot if your model supports images; otherwise explicitly disclose that visual review is unperformed.
74
+ After creating a file, prefer focused edit calls over repeatedly rewriting the full file. Whole-file rewrites bloat model context, increase provider rate-limit risk, and can accidentally remove previously working behavior. If development reveals a wrong assumption or a step can't be completed as planned, call delivery_revise to adjust the contract — update steps and checks to match reality, but never silently drop original acceptance criteria. For multi-package work use structured steps {id,title,dependsOn:[],checks:[]} with stable IDs: dependencies gate progress and mapped checks must pass before done. Plan near-term slices in detail, leave later slices coarse, and refine them as dependencies become clear. Record architecture boundaries, affected callers, compatibility constraints, risky assumptions and rollback strategy in assumptions; resolve uncertainty with focused probes. Establish baseline failures, add a regression that fails before the fix, then run focused tests followed by integration and repository quality gates. Reopening a prerequisite resets downstream progress. Finish requires all structured steps done; string steps remain supported.
75
+ Use a few meaningful check suites (usually 2–4), not one command per criterion: multiple acceptance criteria can share a suite. When user-owned required validators already cover a requirement, do not duplicate them with shallow model-authored checks; add only focused checks for logic they do not cover. Scope honestly: enumerate every explicit requirement in the user's prompt and back each core requirement with an acceptance criterion and a real check. Narrow contracts that omit core requirements make 'verified' a scope failure, not a smaller task; if budget remains once checks pass, implement and verify the missing requirements instead of stopping at the first passing slice. Run delivery_check with id="all" to execute every declared check sequentially. It executes the argv with a deadline, records logs and fingerprints the entire working project (excluding dependencies, caches and generated artifacts), so omitting a source file cannot hide stale evidence. Artifact roots must contain product source/tests/config/docs, never only artifacts/. Use ["."] for the project. Do not edit during checks; run dependent tools in separate batches. Use artifacts/ for generated screenshots/build output; .harness/ is reserved for harness logs. Re-run checks after final edits. When delivery_status reports readyToFinish:true, stop polling: adversarially review the result and call delivery_finish. Repeating an unchanged delivery_status is not progress and is blocked after a small number of calls.
76
+ For a regression-first milestone use {id,title,kind:"regression",checks:[]}; capture an actual behavioral assertion failure (not an import or syntax failure) with bash before implementation and record its command/result in progress notes. Keep green suites on acceptance and the later integration milestone. Run focused intermediate gates rather than waiting until every package has changed. New or remapped acceptance criteria require new test executions even if old command evidence is still fresh; add assertions that would catch the newly required behavior.
77
+ Before concluding a structured plan, call delivery_review action="inspect" to capture the final Git diff. Read the diff and any new files, challenge each criterion with concrete assertion coverage, and run additional boundary/metamorphic probes (for numbers: different magnitudes, signs, extremes, equivalent representations and permutation invariance, not only tiny happy-path examples). Fix findings, rerun checks, and inspect again after edits. Then call delivery_review action="record" with captureId, coverage:[{requirement,assertions}], probes:[command and observed result], findings:[] and limitations. Nonempty findings must be fixed or handed off blocked. The recorded review and exact check evidence are included in the harness handoff; don't invent provenance in a model-authored report. This traceable self-review is not independent proof. Finally call delivery_finish with the review, launch command and honest limitations. Failed, missing or stale checks cannot produce verified status. If genuinely blocked, use status=blocked with the reason; do not weaken tests to manufacture success. Phased delivery means steady progress across many tool calls within the run, not a single response. No automatic deployments or unrequested destructive changes.`;
78
+
79
+ export default function delivery(pi: ExtensionAPI) {
80
+ let state: any = null;
81
+ let discovery = newDiscovery();
82
+ const saveDiscovery = () => pi.appendEntry('delivery-discovery-v1', structuredClone(discovery));
83
+ let touched = false, nudges = 0;
84
+ let budget: any = null;
85
+ let statusPollKey = '', statusPollRepeats = 0;
86
+ const resetStatusPolls = () => { statusPollKey = ''; statusPollRepeats = 0; };
87
+ let budgetTimer: ReturnType<typeof setTimeout> | undefined;
88
+ const saveBudget = () => pi.appendEntry('delivery-budget-v1', structuredClone(budget));
89
+ function stopBudget(reason: string, ctx: ExtensionContext) {
90
+ clearTimeout(budgetTimer);
91
+ if (!budget.stopped) {
92
+ budget.stopped = reason;
93
+ saveBudget();
94
+ pi.sendMessage({ customType: 'delivery-budget-stop', display: true, content: JSON.stringify({
95
+ status: 'budget-exhausted', reason, budget,
96
+ requirements: state?.plan.acceptance || [], evidence: state?.evidence || {},
97
+ next: 'Review incomplete work. Use /delivery-budget-reset to explicitly authorize a new budget.'
98
+ }) }, { triggerTurn: false });
99
+ }
100
+ ctx.abort();
101
+ return { block: true, reason: `Delivery budget exhausted: ${reason}. User reset required.` };
102
+ }
103
+ function ensureBudget(ctx: ExtensionContext, action?: string) {
104
+ if (!budget) {
105
+ const limits = budgetLimits(name => pi.getFlag(name));
106
+ if (!Object.values(limits).some(Boolean)) return;
107
+ budget = newBudget(limits); saveBudget();
108
+ }
109
+ const reason = budgetReason(budget, action);
110
+ if (reason) return stopBudget(reason, ctx);
111
+ if (action === 'tool') { budget.tools++; saveBudget(); }
112
+ if (action === 'repair') { budget.repairs++; saveBudget(); }
113
+ }
114
+ pi.registerCommand('delivery-budget-status', { description: 'Show session budget usage, remaining allowance and stop reason', handler: (_args, ctx) => {
115
+ const content = JSON.stringify(budgetSnapshot(budget));
116
+ pi.sendMessage({ customType: 'delivery-budget-status', display: true, content }, { triggerTurn: false });
117
+ if (ctx.hasUI) ctx.ui.notify(content, 'info');
118
+ } });
119
+ pi.registerCommand('delivery-budget-reset', { description: 'Explicitly authorize a fresh session budget with current configured limits', handler: (_args, ctx) => {
120
+ clearTimeout(budgetTimer);
121
+ budget = newBudget(budgetLimits(name => pi.getFlag(name)));
122
+ saveBudget();
123
+ if (ctx.hasUI) ctx.ui.notify('Delivery budget reset', 'info');
124
+ } });
125
+ for (const name of ['tools', 'seconds', 'repairs']) pi.registerFlag(`delivery-max-${name}`, { description: `Session budget for ${name}; 0 disables this limit. Reset only with /delivery-budget-reset.`, type: 'string', default: '0' });
126
+ pi.registerFlag('delivery-jev', { description: 'Enable Jev plan advice (default on for Carthagent Ship; maximum 3 attempts/run; never approval authority)', type: 'boolean', default: false });
127
+ pi.registerFlag('delivery-jev-provider', { description: 'Jev provider: openrouter (OPENROUTER_API_KEY) or typesafe (TYPESAFE_API_KEY); Carthagent Ship models default to the managed ship endpoint when unset', type: 'string', default: '' });
128
+ pi.registerFlag('delivery-jev-broker', { description: 'Explicit trusted local billing broker http://127.0.0.1:PORT/jev (no provider key sent)', type: 'string' });
129
+ const exclusive = createSerialQueue();
130
+ let contextDirectory: string | undefined;
131
+ let contextCwd: string | undefined;
132
+ let lastContextBytes = 0;
133
+ let contextArchiveError = false;
134
+ let recoveryCheckpoint: Record<string, any> = {};
135
+ let probeState = { failures: 0, family: null };
136
+ const guidanceProfiles = loadGuidanceProfiles();
137
+ let activeGuidance: any[] = [];
138
+ let required: any[] = [], extraGuidance = '', configError = '';
139
+ let writeCounts = new Map<string, number>();
140
+ let initialFiles = new Set<string>();
141
+ pi.registerFlag('delivery-validators', { description: 'Path to user-owned required validator manifest; these checks cannot be omitted by the model', type: 'string' });
142
+ pi.registerFlag('delivery-workflow', { description: 'standard: compact contract plus final checks/review; strict: tested architecture approval', type: 'string', default: 'strict' });
143
+ pi.registerFlag('delivery-context', { description: 'Path to optional task/context guidance injected into the delivery system prompt', type: 'string' });
144
+ pi.registerFlag('delivery-strict', { description: 'Deprecated compatibility flag; tested-plan mutation gate is always enabled (not an OS sandbox)', type: 'boolean', default: true });
145
+ pi.registerFlag('delivery-bash-cap', { description: 'Cap bash/powershell tool timeouts at N seconds (0 disables). A single un-timed runaway command (e.g. find /) can otherwise consume an entire bounded run. Instruct the model to set bounded timeouts either way.', type: 'string', default: '0' });
146
+ pi.registerFlag('delivery-protect-existing', { description: 'Require focused edits instead of whole-file replacement for files present at session start', type: 'boolean', default: false });
147
+ pi.registerFlag('delivery-rewrite-cap', { description: 'Maximum built-in write calls per path in a bounded run; later changes must use focused edits (0 disables)', type: 'string', default: '0' });
148
+ pi.registerFlag('delivery-turn-delay-ms', { description: 'Base delay after tool results in bounded runs, scaled by active context size to reduce provider rate-limit bursts (0 disables)', type: 'string', default: '0' });
149
+ pi.registerFlag('delivery-tool-output-cap', { description: 'Maximum characters retained from each text tool result in bounded runs; preserves the beginning and end (0 disables)', type: 'string', default: '0' });
150
+ function restore(ctx: ExtensionContext) {
151
+ contextCwd = ctx.cwd;
152
+ contextDirectory = undefined;
153
+ lastContextBytes = 0;
154
+ contextArchiveError = false;
155
+ recoveryCheckpoint = {};
156
+ probeState = { failures: 0, family: null };
157
+ clearTimeout(budgetTimer);
158
+ budget = [...ctx.sessionManager.getEntries()].reverse().find((e: any) => e.type === 'custom' && e.customType === 'delivery-budget-v1')?.data || null;
159
+ if (budget) budget = structuredClone(budget);
160
+ budgetLimits(name => pi.getFlag(name));
161
+ discovery = restoreDiscovery([...ctx.sessionManager.getBranch()].reverse().find((e: any) => e.type === 'custom' && e.customType === 'delivery-discovery-v1')?.data);
162
+ state = restoreState(ctx.sessionManager.getBranch());
163
+ if (state) { state = reconcileReport(join(directory(ctx), 'report.json'), state); contextDirectory = directory(ctx); }
164
+ activeGuidance = (state?.guidanceProfiles || state?.plan?.guidanceProfiles || [])
165
+ .map((id: string) => guidanceProfiles.find(profile => profile.id === id))
166
+ .filter(Boolean);
167
+ touched = false;
168
+ nudges = state?.nudges ?? 0;
169
+ resetStatusPolls();
170
+ required = [];
171
+ extraGuidance = '';
172
+ configError = '';
173
+ writeCounts = new Map();
174
+ initialFiles = new Set();
175
+ if (pi.getFlag('delivery-protect-existing')) {
176
+ for (const entry of readdirSync(ctx.cwd, { withFileTypes: true })) {
177
+ if (entry.isFile()) initialFiles.add(resolve(ctx.cwd, entry.name));
178
+ }
179
+ }
180
+ const explicit = pi.getFlag('delivery-validators');
181
+ const projectConfig = join(ctx.cwd, CONFIG_DIR_NAME, 'delivery.json');
182
+ const file = typeof explicit === 'string' && explicit ? resolve(ctx.cwd, explicit) : (ctx.isProjectTrusted?.() && existsSync(projectConfig) ? projectConfig : undefined);
183
+ if (file) {
184
+ try { required = loadRequiredChecks(file, ctx.cwd); }
185
+ catch (error) { configError = `Invalid required validators: ${error}`; }
186
+ }
187
+ const context = pi.getFlag('delivery-context');
188
+ if (typeof context === 'string' && context.trim()) {
189
+ try { extraGuidance = readFileSync(resolve(ctx.cwd, context), 'utf8'); }
190
+ catch (error) { configError = `Invalid delivery context: ${error}`; }
191
+ }
192
+ if (state && required.length) {
193
+ for (const check of required) {
194
+ const previous = state.plan.checks.find((c: any) => c.id === check.id);
195
+ if (JSON.stringify(previous) !== JSON.stringify(check)) delete state.evidence[check.id];
196
+ }
197
+ state.plan = bindRequiredChecks(state.plan, required);
198
+ if (required.some(check => !state.evidence[check.id])) state.status = 'verifying';
199
+ }
200
+ }
201
+ function directory(ctx: ExtensionContext) {
202
+ return localPath(ctx.cwd, join('.harness', ctx.sessionManager.getSessionId(), state.runId));
203
+ }
204
+ function refreshState(ctx: ExtensionContext) {
205
+ if (state) {
206
+ assertActiveReport(ctx.cwd, join(directory(ctx), 'report.json'));
207
+ state = reconcileReport(join(directory(ctx), 'report.json'), state);
208
+ }
209
+ }
210
+ async function workspaceOperation(ctx: ExtensionContext, operation: () => Promise<any>, replace = false) {
211
+ const release = lockWorkspace(ctx.cwd);
212
+ try {
213
+ if (replace) { if (state) state = reconcileReport(join(directory(ctx), 'report.json'), state); }
214
+ else refreshState(ctx);
215
+ return await operation();
216
+ }
217
+ finally { release(); }
218
+ }
219
+ function persist(ctx: ExtensionContext) {
220
+ contextCwd = ctx.cwd;
221
+ contextDirectory = directory(ctx);
222
+ state.nudges = nudges;
223
+ saveReport(join(directory(ctx), 'report.json'), state);
224
+ pi.appendEntry('delivery-state-v1', structuredClone(state));
225
+ if (ctx.hasUI) ctx.ui.setStatus('delivery', `delivery: ${state.status}`);
226
+ }
227
+ pi.on('session_start', (_event, ctx) => restore(ctx));
228
+ pi.on('session_tree', (_event, ctx) => restore(ctx));
229
+ pi.on('session_shutdown', () => clearTimeout(budgetTimer));
230
+ pi.on('agent_start', (_event, ctx) => {
231
+ if (ensureBudget(ctx)) return;
232
+ clearTimeout(budgetTimer);
233
+ if (budget?.limits.seconds) {
234
+ budgetTimer = setTimeout(() => stopBudget('elapsed-time', ctx), Math.max(1, budget.startedAt + budget.limits.seconds * 1000 - Date.now()));
235
+ budgetTimer.unref();
236
+ }
237
+ });
238
+ pi.on('input', event => {
239
+ if (event.source !== 'extension') { nudges = 0; touched = false; resetStatusPolls(); discovery = newDiscovery(); saveDiscovery(); }
240
+ return { action: 'continue' };
241
+ });
242
+ pi.on('before_agent_start', (event, ctx) => {
243
+ const configuredWorkflow = pi.getFlag('delivery-workflow') ?? 'strict';
244
+ if (!['standard', 'strict'].includes(String(configuredWorkflow))) throw Error('delivery-workflow must be standard or strict');
245
+ // An active run keeps the mode it was planned under; the flag only selects the mode for new runs.
246
+ const workflow = state && !['verified', 'blocked'].includes(state.status) ? (state.workflowMode === 'standard' ? 'standard' : 'strict') : configuredWorkflow;
247
+ const routed = routeGuidance(event.prompt, { cwd: ctx?.cwd || process.cwd(), profiles: guidanceProfiles });
248
+ activeGuidance = state && !['verified', 'blocked'].includes(state.status)
249
+ ? [...new Map([...activeGuidance, ...routed].map(profile => [profile.id, profile])).values()].sort((a, b) => b.priority - a.priority)
250
+ : routed;
251
+ if (!discovery.active && (!state || ['verified', 'blocked'].includes(state.status)) && !looksInformational(event.prompt)) {
252
+ discovery = newDiscovery(true); saveDiscovery();
253
+ }
254
+ let guidance = DELIVERY_INVARIANTS + '\n' + deliveryPhaseGuidance(state);
255
+ if (workflow === 'standard') guidance = 'Standard delivery: inspect relevant files (ls/find/read/code_nav); create delivery_plan with complete requirements, artifact roots, a few steps and real checks. A valid required plan enables implementation immediately: design components/scenario ceremony is optional. Revisions merge design/workflow fields, replace supplied arrays, preserve obligations and require fresh final evidence, not repeated approval. Mark implementation steps done independently of integrated check execution. Use delivery_check all, final delivery_review inspect/record, then delivery_finish with honest limitations. No fabricated evidence, scope reduction or unrequested dependency changes. Informational plans do not authorize implementation. For bounded environment versions use delivery_probe kind=node-version or npm-version; inspect other APIs through read.';
256
+ if (/\b(test|typescript|route|backend|api)\b/i.test(event.prompt)) guidance += '\n' + TEST_INFRASTRUCTURE_GUIDANCE;
257
+ if (activeGuidance.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(event.prompt)) guidance += BROWSER_CHECK_GUIDANCE;
258
+ const supportsImages = !Array.isArray(ctx?.model?.input) || ctx.model.input.includes('image');
259
+ if (!supportsImages) guidance += `\nThis session's model cannot process images: do not attempt to view, read, or reason over image files or screenshots — visual review is unavailable. Verify visual behavior through DOM structure, computed styles, and console evidence instead; screenshots remain useful as evidence artifacts for the user even though you cannot inspect them.`;
260
+ if (discovery.active && workflow !== 'standard') guidance += '\n\n' + PLANNING_START;
261
+ const routedText = formatGuidance(activeGuidance);
262
+ if (routedText) guidance += `\n\n${routedText}`;
263
+ if (extraGuidance) guidance += `\n\nTask-specific delivery context:\n${extraGuidance}`;
264
+ if (required.length) guidance += `\nUser-owned required validators will be added to your plan automatically: ${JSON.stringify(required)}. Run delivery_check id="all"; repair failures rather than replacing or bypassing these checks.`;
265
+ if (looksInformational(event.prompt)) guidance += `\n\nReceipt check: this prompt reads as informational/read-only. Answer it directly, or if you record a contract use delivery_plan verification:"none" (no checks) or "advisory" (optional non-blocking evidence). Do not enter the verify/repair loop for a pure question — decide the classification now, before the verify loop.`;
266
+ return { systemPrompt: event.systemPrompt + '\n\n' + guidance };
267
+ });
268
+ pi.on('session_compact', (_event, ctx) => { if (state) refreshState(ctx); });
269
+ pi.on('context', (event, ctx) => {
270
+ // Generated compaction summaries never confer authority. Refresh this exact run.
271
+ if (state && ctx) refreshState(ctx);
272
+ if (!state || ['verified', 'blocked'].includes(state.status)) {
273
+ const checkpoint = discoveryCheckpoint(discovery);
274
+ if (checkpoint) return { messages: [...event.messages, { role: 'custom' as const, customType: 'delivery-discovery', content: checkpoint, display: false, timestamp: Date.now() }] };
275
+ return;
276
+ }
277
+ const report = contextDirectory ? join(contextDirectory, 'report.json') : undefined;
278
+ const summary = deliveryRecovery(state, { report });
279
+ const archived: any[] = [];
280
+ for (const group of archivalGroups(event.messages, { allow: Boolean(state.planning?.approval), keepRecent: 2 })) {
281
+ if (!contextCwd || !contextDirectory) break;
282
+ try { archived.push({ ...group, archive: archiveContext(contextCwd, contextDirectory, group.messages) }); }
283
+ catch { contextArchiveError = true; /* Retain original messages on archive failure. */ }
284
+ }
285
+ const messages = replaceArchivedGroups(event.messages, archived);
286
+ const pressure = lastContextBytes > 240000
287
+ ? '\nContext pressure: finish the current runnable slice and reserve room for checks/review. Use native compaction if enabled; do not drop requirements or start optional work. This warning does not enable paid compaction.' : '';
288
+ const projected = recoveryMessages(messages, summary, recoveryCheckpoint);
289
+ projected.at(-1).content += `\nInspect freshness before execution/finish; readyToFinish means finish, not polling.${pressure}${contextArchiveError ? '\nArchive unavailable: affected originals retained.' : ''}\n${deliveryPhaseGuidance(state)}`;
290
+ return { messages: projected };
291
+ });
292
+ pi.on('before_provider_request', event => {
293
+ if (!contextDirectory || !contextCwd) return;
294
+ const metrics = contextMetrics(event.payload);
295
+ const growthBytes = metrics.totalBytes - lastContextBytes;
296
+ lastContextBytes = metrics.totalBytes;
297
+ try { appendFileSync(localPath(contextCwd, join(contextDirectory, 'context-metrics.jsonl')), JSON.stringify({ ...metrics, growthBytes, at: new Date().toISOString() }) + '\n', { mode: 0o600 }); }
298
+ catch { /* Numeric telemetry must not mutate gates or block work. */ }
299
+ });
300
+ pi.on('tool_call', (event, ctx) => {
301
+ const stopped = ensureBudget(ctx, 'tool');
302
+ if (stopped) return stopped;
303
+ if (event.toolName === 'delivery_status') {
304
+ let key = '';
305
+ if (state) {
306
+ try {
307
+ refreshState(ctx);
308
+ const hash = fingerprint(ctx.cwd, ['.']);
309
+ key = JSON.stringify({ runId: state.runId, revision: state.revision, status: state.status, pending: pendingChecks(state, hash), hash });
310
+ } catch (err: any) {
311
+ key = JSON.stringify({ runId: state?.runId, revision: state?.revision, status: state?.status, error: err?.message || String(err) });
312
+ }
313
+ }
314
+ if (key && key === statusPollKey) {
315
+ // Keep blocked retries blocked, but recompute the key first so a real
316
+ // report/workspace change automatically permits a fresh status read.
317
+ if (statusPollRepeats >= 3) {
318
+ return { block: true, reason: 'Repeated unchanged delivery_status calls are blocked. Use the last result: call delivery_finish when readyToFinish is true; otherwise edit, run delivery_check, or report status=blocked. Do not poll again until state or workspace changes.' };
319
+ }
320
+ statusPollRepeats++;
321
+ } else { statusPollKey = key; statusPollRepeats = key ? 1 : 0; }
322
+ } else if (event.toolName.startsWith('delivery_') || ['write', 'edit'].includes(event.toolName)) {
323
+ resetStatusPolls();
324
+ }
325
+ const input = event.input as Record<string, any> | undefined;
326
+ if (
327
+ event.toolName === 'edit' &&
328
+ input &&
329
+ typeof input === 'object' &&
330
+ typeof input.edits === 'string'
331
+ ) {
332
+ try {
333
+ const edits = JSON.parse(input.edits);
334
+ if (
335
+ Array.isArray(edits) &&
336
+ edits.every(edit => edit && typeof edit === 'object' && !Array.isArray(edit))
337
+ ) {
338
+ input.edits = edits;
339
+ }
340
+ } catch {
341
+ // Leave invalid input unchanged so the tool returns its normal validation error.
342
+ }
343
+ }
344
+ if (needsImplementation(event.toolName)) {
345
+ try {
346
+ if (!ctx?.cwd) throw Error('Implementation locked: call delivery_plan and test/approve its design first.');
347
+ const release = lockWorkspace(ctx.cwd);
348
+ try {
349
+ refreshState(ctx);
350
+ startImplementation(state, fingerprint(ctx.cwd, ['.']));
351
+ persist(ctx);
352
+ } finally { release(); }
353
+ } catch (error: any) {
354
+ if (discovery.active) { discovery = discoveryEvent(discovery, 'blocked'); saveDiscovery(); }
355
+ return { block: true, reason: error.message + (discoveryCheckpoint(discovery) ? '\n' + discoveryCheckpoint(discovery) : '') };
356
+ }
357
+ }
358
+ if (['write', 'edit'].includes(event.toolName) && input && typeof input === 'object') {
359
+ const path = typeof input.path === 'string' ? input.path.replaceAll('\\', '/') : '';
360
+ const cwdAsRelativePath = ctx?.cwd?.replaceAll('\\', '/').replace(/^\/+/, '') ?? '';
361
+ const cwdName = ctx?.cwd ? basename(ctx.cwd).replaceAll('\\', '/') : '';
362
+ if (
363
+ cwdAsRelativePath &&
364
+ (path === cwdAsRelativePath ||
365
+ path.startsWith(`${cwdAsRelativePath}/`) ||
366
+ path === cwdName ||
367
+ path.startsWith(`${cwdName}/`))
368
+ ) {
369
+ return {
370
+ block: true,
371
+ reason: 'This path recreates the working directory inside itself. Use a workspace-relative product path such as README.md or src/index.ts.',
372
+ };
373
+ }
374
+ if (
375
+ event.toolName === 'write' &&
376
+ ctx?.cwd &&
377
+ initialFiles.has(resolve(ctx.cwd, path))
378
+ ) {
379
+ return {
380
+ block: true,
381
+ reason: `Preserve the existing ${path} implementation. Inspect it and use focused edit calls instead of replacing the whole file.`,
382
+ };
383
+ }
384
+ }
385
+ const rewriteCap = Math.max(0, Number(pi.getFlag('delivery-rewrite-cap')) || 0);
386
+ if (rewriteCap > 0 && event.toolName === 'write' && input && typeof input === 'object') {
387
+ const path = typeof input.path === 'string' ? input.path : '';
388
+ if (path) {
389
+ const count = writeCounts.get(path) ?? 0;
390
+ if (count >= rewriteCap) {
391
+ return {
392
+ block: true,
393
+ reason: `Full-file rewrite limit reached for ${path}. Preserve working behavior and use focused edit calls for subsequent changes.`,
394
+ };
395
+ }
396
+ writeCounts.set(path, count + 1);
397
+ }
398
+ }
399
+ // Bounded-run protection: shell tools have no default timeout, so one un-timed
400
+ // runaway command (observed live: `find /`) can block until the wall clock expires.
401
+ // Mutating input is a documented pi guarantee and affects execution.
402
+ const cap = Number(pi.getFlag('delivery-bash-cap')) || 0;
403
+ if (['bash', 'powershell'].includes(event.toolName) && input && typeof input === 'object') {
404
+ const command = typeof input.command === 'string' ? input.command : '';
405
+ if (ctx?.cwd && initialFiles.size) {
406
+ const redirects = [...command.matchAll(/(?:^|[^>])>(?!>)\s*(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/g)]
407
+ .map(match => match[1] ?? match[2] ?? match[3])
408
+ .filter(Boolean)
409
+ .map(path => resolve(ctx.cwd, path));
410
+ if (redirects.some(path => initialFiles.has(path))) {
411
+ return {
412
+ block: true,
413
+ reason: 'Shell redirection cannot replace a file that existed at session start. Inspect it and use a focused edit tool instead.',
414
+ };
415
+ }
416
+ const replacesWithScript =
417
+ /\bopen\s*\([^)]*,\s*['"][wax+]+['"]/i.test(command) ||
418
+ /\b(?:write_text|writeFileSync|writeFile)\s*\(/i.test(command);
419
+ if (
420
+ replacesWithScript &&
421
+ [...initialFiles].some(path => command.includes(path) || command.includes(basename(path)))
422
+ ) {
423
+ return {
424
+ block: true,
425
+ reason: 'A script cannot replace a file that existed at session start. Inspect it and use a focused edit tool instead.',
426
+ };
427
+ }
428
+ const removesOrReplaces = /\b(?:rm|mv|touch|truncate)\b/i.test(command);
429
+ if (
430
+ removesOrReplaces &&
431
+ [...initialFiles].some(path => command.includes(path) || command.includes(basename(path)))
432
+ ) {
433
+ return {
434
+ block: true,
435
+ reason: 'Shell commands cannot remove or replace a file that existed at session start. Inspect it and use a focused edit tool instead.',
436
+ };
437
+ }
438
+ }
439
+ const broadTermination =
440
+ /\b(?:pkill|killall)\b/i.test(command) ||
441
+ /\bkill\b\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(command) ||
442
+ /\btaskkill\b[^\n]*(?:\/im|\*)/i.test(command) ||
443
+ /\bstop-process\b[^\n]*\s-name\b/i.test(command) ||
444
+ /\bget-process\b[^\n]*\|\s*stop-process\b/i.test(command);
445
+ if (broadTermination) {
446
+ return { block: true, reason: 'Broad process termination is prohibited in bounded delivery runs. Capture the launched child PID and terminate only that PID.' };
447
+ }
448
+ if (cap > 0 && (typeof input.timeout !== 'number' || input.timeout > cap)) input.timeout = cap;
449
+ }
450
+ });
451
+ pi.on('tool_result', async (event, ctx) => {
452
+ const diagnostic = probeDiagnostics(probeState, event);
453
+ probeState = diagnostic.state;
454
+ if (discovery.active && ['read', 'ls', 'find', 'grep', 'code_nav'].includes(event.toolName)) { discovery = discoveryEvent(discovery, 'read'); saveDiscovery(); }
455
+ if (!event.isError && ['write', 'edit'].includes(event.toolName)) touched = true;
456
+ const baseDelay = Math.max(0, Number(pi.getFlag('delivery-turn-delay-ms')) || 0);
457
+ const contextTokens = ctx?.getContextUsage?.()?.tokens ?? 0;
458
+ const delay = Math.min(30000, baseDelay * Math.max(1, Math.ceil(contextTokens / 50000)));
459
+ if (delay > 0) await new Promise(resolve => setTimeout(resolve, delay));
460
+ const cap = Math.max(0, Number(pi.getFlag('delivery-tool-output-cap')) || 0);
461
+ if (cap > 0 && event.content) {
462
+ let changed = false;
463
+ const content = event.content.map(item => {
464
+ if (item.type !== 'text' || item.text.length <= cap) return item;
465
+ changed = true;
466
+ const marker = `\n...[${item.text.length - cap} characters omitted; rerun a narrower command if needed]...\n`;
467
+ if (cap <= marker.length) return { ...item, text: item.text.slice(0, cap) };
468
+ const head = Math.floor((cap - marker.length) / 3);
469
+ const tail = cap - marker.length - head;
470
+ return { ...item, text: item.text.slice(0, head) + marker + item.text.slice(-tail) };
471
+ });
472
+ if (changed || diagnostic.message) return { content: diagnostic.message ? [...content, { type: 'text' as const, text: diagnostic.message }] : content };
473
+ }
474
+ if (diagnostic.message) return { content: [...(event.content || []), { type: 'text' as const, text: diagnostic.message }] };
475
+ });
476
+
477
+ pi.registerTool({
478
+ name: 'delivery_plan', label: 'Delivery plan', description: 'Create an acceptance contract and generate plan.d2. Use delivery_revise for ongoing work; replacing an active plan is treated as a revision. After verified/blocked, this starts a new task. Paths are relative files/directories, not globs. Checks use executable argv (no implicit shell). Set verification:"none" for a pure informational answer, "advisory" for optional non-blocking evidence, or leave the default "required" for tasks that change product files.',
479
+ parameters: Type.Object(planFields),
480
+ async execute(_id, params, _signal, _update, ctx) {
481
+ return exclusive(() => workspaceOperation(ctx, async () => {
482
+ if (configError) throw Error(configError);
483
+ const plan = validatePlan(bindRequiredChecks({ ...params, verification: params.verification ?? 'required' }, required), ctx.cwd);
484
+ validateRevision(state, plan);
485
+ if (state && !['verified', 'blocked'].includes(state.status)) {
486
+ let hash: string | undefined;
487
+ try { hash = fingerprint(ctx.cwd, ['.']); } catch { /* retain stale evidence without rebinding */ }
488
+ state = revisePlan(state, plan, { cwd: ctx.cwd, hash, reason: 'Active plan replaced via delivery_plan' });
489
+ } else {
490
+ const workflowMode = pi.getFlag('delivery-workflow') === 'standard' ? 'standard' : 'strict';
491
+ state = { version: 1, workflowMode, reviewRequired: workflowMode === 'standard', runId: randomUUID(), revision: (state?.revision || 0) + 1, guidanceProfiles: activeGuidance.map(profile => profile.id), plan, evidence: {}, status: 'implementing', createdAt: new Date().toISOString(), stepStatus: {} };
492
+ }
493
+ discovery = newDiscovery(); saveDiscovery();
494
+ const dir = directory(ctx);
495
+ mkdirSync(dir, { recursive: true });
496
+ writeFileSync(join(dir, 'plan.d2'), planD2WithProgress(state.plan, state.stepStatus));
497
+ persist(ctx);
498
+ selectReport(ctx.cwd, join(dir, 'report.json'));
499
+ return text({ plan: join(dir, 'plan.d2'), report: join(dir, 'report.json'), next: state.workflowMode === 'standard' ? 'Contract accepted. Implement now; run declared checks and final snapshot review before finish.' : 'No code or commands yet. delivery_design guide gives exact fields. Validate, inspect the capture (optional Jev advice), review, then approve. Revise blocking findings first.' });
500
+ }, true));
501
+ },
502
+ });
503
+ pi.registerTool({
504
+ name: 'delivery_probe', label: 'Environment discovery probe',
505
+ description: 'Bounded pre-plan environment discovery. node-version/npm-version only; no arbitrary shell, scripts or mutation. Other installed APIs can be inspected with read. Not check evidence.',
506
+ parameters: Type.Object({ kind: Type.String({ enum: ['node-version', 'npm-version'] }) }),
507
+ async execute(_id, params, signal, _update, ctx) {
508
+ const argv = params.kind === 'node-version' ? [process.execPath, '--version'] : params.kind === 'npm-version' ? [process.platform === 'win32' ? 'npm.cmd' : 'npm', '--version'] : null;
509
+ if (!argv) throw Error('Unknown discovery probe; use node-version or npm-version');
510
+ return text({ ...(await runCommand(argv, { cwd: ctx.cwd, signal, timeoutSeconds: 10 })), evidence: false });
511
+ },
512
+ });
513
+ pi.registerTool({
514
+ name: 'delivery_revise', label: 'Revise delivery plan',
515
+ description: 'Adapt the current task with a reason and partial plan patch. Omitted fields are preserved; supplied arrays replace that field. Keeps run identity, revision history and unambiguous step progress. Only unchanged checks on unchanged source retain fresh evidence. Cannot drop acceptance criteria or erase verification history, even when resuming blocked work.',
516
+ parameters: Type.Object({ reason: shortString(), patch: Type.Partial(Type.Object(planFields)) }),
517
+ async execute(_id, params, _signal, _update, ctx) {
518
+ return exclusive(() => workspaceOperation(ctx, async () => {
519
+ if (configError) throw Error(configError);
520
+ if (!state) throw Error('Call delivery_plan first');
521
+ let hash: string | undefined;
522
+ try { hash = fingerprint(ctx.cwd, ['.']); } catch { /* revisions remain possible at scope limits */ }
523
+ const bound = bindRequiredChecks(mergePlanPatch(state.plan, params.patch), required);
524
+ state = revisePlan(state, bound, { cwd: ctx.cwd, reason: params.reason, hash });
525
+ persist(ctx);
526
+ return text({ revision: state.revision, report: join(directory(ctx), 'report.json'), stepStatus: state.stepStatus, pendingChecks: pendingChecks(state, hash), unverifiedRequirements: pendingRequirements(state, hash), next: state.workflowMode === 'standard' ? 'Revised contract accepted. Continue implementation; changed checks/requirements need fresh execution and final review.' : 'Revision relocked implementation. Revalidate, review and approve the design before new code or check execution.' });
527
+ }));
528
+ },
529
+ });
530
+ pi.registerTool({
531
+ name: 'delivery_design', label: 'Test delivery design',
532
+ description: 'Use action=guide BEFORE planning for compact exact fields and an illustrative example, without state or approval. Before generating code: validate architecture and scenarios, inspect, record adversarial review, then approve. Review is self-reported, not independent proof. Revisions and preimplementation source edits invalidate approval.',
533
+ parameters: Type.Object({ action: Type.String({ enum: ['guide', 'validate', 'inspect', 'review', 'approve'] }), review: Type.Optional(Type.Object({
534
+ captureId: Type.Optional(shortString()), challenges: Type.Array(shortString()),
535
+ walkthroughs: Type.Array(Type.Object({ scenario: shortString(), trace: shortString(), assertion: shortString() })),
536
+ findings: Type.Array(Type.Object({ severity: Type.String({ enum: ['blocking', 'advisory'] }), description: shortString(), disposition: Type.Optional(shortString()) })),
537
+ limitations: Type.Array(shortString()), resolutions: Type.Optional(Type.Array(Type.Object({ id: shortString(), change: shortString(), evidence: shortString() }))),
538
+ })), captureId: Type.Optional(shortString()) }),
539
+ async execute(_id, params, _signal, _update, ctx) {
540
+ if (params.action === 'guide') return text(planningGuide());
541
+ return exclusive(() => workspaceOperation(ctx, async () => {
542
+ if (!state) throw Error('Call delivery_plan first; action=guide gives exact fields.');
543
+ const hash = fingerprint(ctx.cwd, ['.']);
544
+ let result;
545
+ if (params.action === 'validate') result = validatePlanning(state, hash);
546
+ else if (params.action === 'inspect') result = inspectPlanning(state, hash);
547
+ else if (params.action === 'review') {
548
+ // delivery_review accepts captureId beside the action; models often
549
+ // mirror that shape, flatten the review fields, or echo the inspect
550
+ // field name `id`. Normalize all three into params.review.
551
+ const review = params.review && typeof params.review === 'object' ? params.review : {};
552
+ for (const field of ['captureId', 'challenges', 'walkthroughs', 'findings', 'limitations', 'resolutions']) {
553
+ if (review[field] == null && params[field] !== undefined) review[field] = params[field];
554
+ }
555
+ if (review.captureId == null && typeof review.id === 'string' && review.id) review.captureId = review.id;
556
+ result = recordPlanReview(state, hash, review);
557
+ }
558
+ else if (params.action === 'approve') result = approvePlanning(state, hash);
559
+ else throw Error('Unknown design action');
560
+ persist(ctx);
561
+ // Carthagent Ship gets managed plan advice by default: the backend
562
+ // proxies the free System One endpoint outside billing. A literal
563
+ // --delivery-jev=false opts out; an explicit --delivery-jev-provider
564
+ // keeps BYOK providers (OpenRouter/TypeSafe) working as before.
565
+ const jevFlag = pi.getFlag('delivery-jev');
566
+ const jevProvider = pi.getFlag('delivery-jev-provider');
567
+ const jevBroker = pi.getFlag('delivery-jev-broker');
568
+ const shipManaged = ctx?.model?.provider === CARTHAGENT_SHIP_PROVIDER_ID;
569
+ const useShipAdvisor = shipManaged && !jevProvider && !jevBroker;
570
+ const advice = params.action === 'inspect' ? await inspectAdvice(state, result, {
571
+ enabled: jevFlag === true || (jevFlag === false && useShipAdvisor),
572
+ createAdvisor: () => useShipAdvisor
573
+ ? createShipAdvisor({ signal: _signal })
574
+ : configuredAdvisor({ enabled: true, provider: jevProvider || 'openrouter', broker: jevBroker, signal: _signal }),
575
+ persist: () => persist(ctx),
576
+ }) : undefined;
577
+ const response = { result: designReceipt(params.action, result), advice, ...(params.action === 'inspect' ? { captureId: result.id, next: `Read the capture, then call delivery_design action=review with review:{captureId:\"${result.id}\", challenges, walkthroughs (one per scenario), findings, limitations}.` } : {}), ...(params.action === 'validate' ? planningNext(state, result) : {}), planningStatus: planningStatus(state, hash), report: join(directory(ctx), 'report.json') };
578
+ if (params.action === 'inspect' && (jsonBytes(response) > 40000 || JSON.stringify(response, null, 2).split('\n').length > 900)) {
579
+ const archive = archiveContext(ctx.cwd, directory(ctx), result);
580
+ const identity = { id: result.id, captureId: result.id, runId: result.runId, revision: result.revision, digest: result.digest, fingerprint: result.fingerprint, provenance: result.provenance };
581
+ const output = text({ result: identity, advice, captureArchive: archive, planningStatus: response.planningStatus, next: `Capture too large for one response. Read the entire captureArchive.path in pages, then call delivery_design action=review with review:{captureId:"${result.id}", ...}. No plan content is silently truncated.` });
582
+ return { ...output, details: { deliveryCapture: result } };
583
+ }
584
+ return text(response);
585
+ }));
586
+ },
587
+ });
588
+ pi.registerTool({
589
+ name: 'delivery_status', label: 'Delivery status', description: 'Show failed/missing/stale checks and the next action. If readyToFinish is true, call delivery_finish instead of polling again.', parameters: Type.Object({}),
590
+ async execute(_id, _params, _signal, _update, ctx) {
591
+ if (!state) return text('No delivery contract.');
592
+ refreshState(ctx);
593
+ let pending: string[] = [];
594
+ let freshnessKnown = true;
595
+ let issues: any = null;
596
+ let designStatus: any = { locked: true, reasons: ['Source fingerprint unavailable.'] };
597
+ try {
598
+ const hash = fingerprint(ctx.cwd, ['.']);
599
+ designStatus = planningStatus(state, hash);
600
+ pending = pendingChecks(state, hash);
601
+ issues = completionIssues(state, ctx.cwd, hash);
602
+ } catch (err: any) {
603
+ freshnessKnown = false;
604
+ pending = [`[scope error: ${err.message}]`];
605
+ }
606
+ const readyToFinish = freshnessKnown && issues && Object.values(issues).every((items: any) => items.length === 0);
607
+ const next = readyToFinish
608
+ ? 'All required checks are fresh and passing. Adversarially review the acceptance criteria, then call delivery_finish with status="verified". Do not call delivery_status again unless the workspace changes.'
609
+ : pending.length
610
+ ? `Run delivery_check for pending checks: ${pending.join(', ')}; repair failures before finishing.`
611
+ : 'Complete outstanding steps/outputs and unverified requirements. For structured plans, use delivery_review action=inspect then action=record before delivery_finish.';
612
+ // History lives in report.json; do not flood model context with old snapshots.
613
+ const current = deliveryRecovery(state, { report: join(directory(ctx), 'report.json'), planningStatus: designStatus });
614
+ return text({ ...current, planningStatus: designStatus, revisionHistory: state.revisions?.map((r: any) => ({ revision: r.revision, reason: r.reason, at: r.at })), pendingChecks: pending, completionIssues: issues, readyToFinish, next });
615
+ },
616
+ });
617
+ pi.registerTool({
618
+ name: 'delivery_progress', label: 'Update step progress', description: 'Mark a plan step as active, done, or failed. Regenerates plan.d2 so the side panel stays current. Use as each step completes or hits a blocker.',
619
+ parameters: Type.Object({
620
+ step: Type.Union([Type.Integer({ minimum: 0 }), shortString()]),
621
+ status: Type.String({ description: 'active, done, failed, or pending' }),
622
+ note: Type.Optional(shortString()),
623
+ }),
624
+ async execute(_id, params, _signal, _update, ctx) {
625
+ return exclusive(() => workspaceOperation(ctx, async () => {
626
+ if (!state) throw Error('Call delivery_plan first');
627
+ const step = planSteps(state.plan).find((s: any, i: number) => typeof params.step === 'string' ? s.id === params.step : i === params.step);
628
+ const hash = params.status === 'done' && step?.checks.length ? fingerprint(ctx.cwd, ['.']) : undefined;
629
+ updateStepStatus(state, params.step, params.status, hash);
630
+ if (params.note) {
631
+ state.progressNotes = [...(state.progressNotes || []), { step: step?.id, status: params.status, note: params.note, revision: state.revision, at: new Date().toISOString() }];
632
+ }
633
+ const dir = directory(ctx);
634
+ writeFileSync(join(dir, 'plan.d2'), planD2WithProgress(state.plan, state.stepStatus));
635
+ persist(ctx);
636
+ return text({ step: params.step, status: params.status, plan: join(dir, 'plan.d2') });
637
+ }));
638
+ },
639
+ });
640
+ pi.registerTool({
641
+ name: 'delivery_check', label: 'Run delivery check', description: 'Execute a declared check, or id="all" to run all checks sequentially. Records real exit status, deadline, logs and source fingerprint. Last 12,000 characters per check, full log capped at 8 MiB. Sibling delivery calls are queued safely; do not edit during checks.',
642
+ parameters: Type.Object({ id: shortString() }),
643
+ async execute(_id, params, signal, onUpdate, ctx) {
644
+ return exclusive(() => workspaceOperation(ctx, async () => {
645
+ if (!state) throw Error('Call delivery_plan first');
646
+ const verification = verificationMode(state.plan);
647
+ // Pre-verify gate: an informational plan has no checks by design. Return a
648
+ // clear route to finish instead of an error that could start a repair loop.
649
+ if (verification === 'none') {
650
+ return text({ verification, note: 'This plan declares verification:"none" (informational), so there are no checks to run. Call delivery_finish with status="verified" and the answer. If evidence is genuinely needed, replan with verification:"advisory" before any check has run.' });
651
+ }
652
+ startImplementation(state, fingerprint(ctx.cwd, ['.']));
653
+ persist(ctx);
654
+ const checks = params.id === 'all' ? state.plan.checks : state.plan.checks.filter((c: any) => c.id === params.id);
655
+ if (!checks.length) throw Error('Unknown check ID. Use delivery_status or id="all".');
656
+ const results = [];
657
+ const failures = [];
658
+ for (const check of checks) {
659
+ const identity = evidenceIdentity(state, check);
660
+ if (signal?.aborted) throw Error('Check cancelled before execution');
661
+ state.status = 'verifying';
662
+ state.verificationStarted = true;
663
+ delete state.evidence[check.id];
664
+ let before: string;
665
+ try {
666
+ before = fingerprint(ctx.cwd, ['.']);
667
+ } catch (err: any) {
668
+ state.evidence[check.id] = {
669
+ passed: false,
670
+ fingerprint: null,
671
+ code: 1,
672
+ timedOut: false,
673
+ cancelled: false,
674
+ logPath: null,
675
+ durationMs: 0,
676
+ changedDuringCheck: false,
677
+ outputTail: `Evidence scope error: ${err.message}`
678
+ };
679
+ persist(ctx);
680
+ throw Error(`Evidence scope error: ${err.message}. If the repository exceeds file limits or cannot be scoped, call delivery_finish with status="blocked" and specific limitations rather than retrying delivery_check.`);
681
+ }
682
+ persist(ctx);
683
+ onUpdate?.(text(`Running ${check.id}: ${JSON.stringify(check.argv)}`));
684
+ const result = await runCommand(check.argv, { cwd: ctx.cwd, timeoutSeconds: check.timeoutSeconds, signal, logPath: join(directory(ctx), `${check.id}-${randomUUID()}.log`) });
685
+ let after: string;
686
+ try {
687
+ after = fingerprint(ctx.cwd, ['.']);
688
+ } catch (err: any) {
689
+ state.evidence[check.id] = {
690
+ passed: false,
691
+ fingerprint: null,
692
+ code: result.code,
693
+ timedOut: result.timedOut,
694
+ cancelled: result.cancelled,
695
+ logPath: result.logPath,
696
+ durationMs: result.durationMs,
697
+ changedDuringCheck: false,
698
+ outputTail: `Post-check evidence scope error: ${err.message}\n${result.output.slice(-800)}`
699
+ };
700
+ persist(ctx);
701
+ throw Error(`Post-check evidence scope error: ${err.message}. If the repository exceeds file limits, call delivery_finish with status="blocked" and specific limitations.`);
702
+ }
703
+ const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after;
704
+ state.evidence[check.id] = { passed, ...identity, executedRevision: state.revision, fingerprint: after, code: result.code, timedOut: result.timedOut, cancelled: result.cancelled, logPath: result.logPath, durationMs: result.durationMs, changedDuringCheck: before !== after, outputTail: result.output.slice(-1200) };
705
+ persist(ctx);
706
+ const message = { id: check.id, advisory: verification !== 'required', ...state.evidence[check.id], outputTail: result.output };
707
+ // Advisory checks are evidence for an informational answer: record the
708
+ // real failure but do not force the repair loop.
709
+ if (!passed && verification === 'required') failures.push(message);
710
+ results.push(message);
711
+ }
712
+ if (failures.length) throw Error(JSON.stringify({ failures, next: 'All requested checks ran. Repair the failures or revise the plan based on findings; do not weaken acceptance criteria.' }, null, 2));
713
+ let finalHash: string | null = null;
714
+ let pending: string[] = [];
715
+ let freshnessError = '';
716
+ try {
717
+ finalHash = fingerprint(ctx.cwd, ['.']);
718
+ pending = pendingChecks(state, finalHash);
719
+ } catch (err: any) {
720
+ freshnessError = err?.message || String(err);
721
+ pending = [`[scope error: ${freshnessError}]`];
722
+ }
723
+ const issues = finalHash !== null ? completionIssues(state, ctx.cwd, finalHash) : null;
724
+ const readyToFinish = issues !== null && Object.values(issues).every((items: any) => items.length === 0);
725
+ return text({ results, pendingChecks: pending, completionIssues: issues, readyToFinish, next: readyToFinish ? 'All required checks are fresh and passing. Adversarially review the acceptance criteria, then call delivery_finish with status="verified". Do not call delivery_status first.' : freshnessError ? 'Evidence freshness could not be established. Call delivery_finish with status="blocked" and the scope limitation.' : verification !== 'required' ? 'Review the recorded advisory evidence and call delivery_finish; failures are context, not repair orders.' : 'Complete pending steps and outputs, and repair or run remaining checks before delivery_finish.' });
726
+ }));
727
+ },
728
+ });
729
+ pi.registerTool({
730
+ name: 'delivery_review', label: 'Review delivery evidence',
731
+ description: 'Required for structured delivery plans. inspect captures the Git diff and new source contents, excluding dependencies/generated output. Read the full capture when truncated. record binds assertion coverage and executed probes to the snapshot. Test critical component connections and the user outcome, not just component functions or artifact existence. Mark undemonstrated requirements coverage.status=unverified. Findings persist; clearing findings or moving them into limitations cannot resolve them. Resolve using resolutions:[{id,change,evidence}] after repair/rechecking or concrete evidence that the original requirement is met; otherwise finish blocked. This is model-authored review, not independent correctness proof.',
732
+ parameters: Type.Object({
733
+ action: Type.String({ description: 'inspect or record' }),
734
+ captureId: Type.Optional(shortString()),
735
+ coverage: Type.Optional(Type.Array(Type.Object({ requirement: shortString(), assertions: shortString(), status: Type.Optional(Type.String({ description: 'demonstrated or unverified' })) }), { maxItems: 64 })),
736
+ resolutions: Type.Optional(Type.Array(Type.Object({ id: shortString(), change: shortString(), evidence: shortString() }), { maxItems: 40 })),
737
+ outcomes: Type.Optional(Type.Array(Type.Object({ id: shortString(), check: shortString(), observation: shortString(), challenge: shortString(), status: Type.String({ description: 'demonstrated or unverified' }) }), { maxItems: 32, description: 'One per plan outcome; check must name its fresh executed check. Observation must describe the actual normal entrypoint result. Challenge records an executed probe of the plausible missed failure (e.g. input-to-persistence or representation roundtrip), not a claim based on source text. Missing core behavior must be unverified.' })),
738
+ probes: Type.Optional(Type.Array(shortString(), { maxItems: 40 })),
739
+ findings: Type.Optional(Type.Array(shortString(), { maxItems: 40 })),
740
+ limitations: Type.Optional(Type.Array(shortString(), { maxItems: 20 })),
741
+ }),
742
+ async execute(_id, params, signal, _update, ctx) {
743
+ return exclusive(() => workspaceOperation(ctx, async () => {
744
+ if (!state) throw Error('Call delivery_plan first');
745
+ let result;
746
+ if (params.action === 'inspect') result = await captureDeliveryReview(state, ctx.cwd, directory(ctx), signal);
747
+ else if (params.action === 'record') { const review = recordDeliveryReview(state, ctx.cwd, params); result = review.recorded === false ? review : { recorded: true, next: 'If steps and outputs are complete, call delivery_finish. Review records model-authored assertions, not a correctness guarantee.' }; }
748
+ else throw Error('Review action must be inspect or record');
749
+ persist(ctx);
750
+ return text(result);
751
+ }));
752
+ },
753
+ });
754
+ pi.registerTool({
755
+ name: 'delivery_finish', label: 'Finish delivery', description: 'Record review and handoff. verified requires all declared checks passing on current artifact hashes for verification:"required" contracts; informational/advisory contracts may verify once with recorded advisory evidence. blocked records an honest incomplete result without pretending success.',
756
+ parameters: Type.Object({ status: Type.String({ description: 'verified or blocked' }), review: shortString(), launch: shortString(), limitations: Type.Array(shortString(), { maxItems: 20 }) }),
757
+ async execute(_id, params, _signal, _update, ctx) {
758
+ return exclusive(() => workspaceOperation(ctx, async () => {
759
+ if (!state) throw Error('Call delivery_plan first');
760
+ if (configError) throw Error(configError);
761
+ if (!['verified', 'blocked'].includes(params.status)) throw Error('status must be verified or blocked');
762
+ const verification = verificationMode(state.plan);
763
+ const advisory = verification !== 'required';
764
+ let hash: string | null = null;
765
+ if (params.status === 'verified') {
766
+ if (advisory) {
767
+ // Informational finish: freshness is not a hard gate, but a best-effort
768
+ // fingerprint keeps later edits visible in the durable report.
769
+ try { hash = fingerprint(ctx.cwd, ['.']); } catch { hash = null; }
770
+ } else {
771
+ hash = fingerprint(ctx.cwd, ['.']);
772
+ const issues = completionIssues(state, ctx.cwd, hash);
773
+ if (Object.values(issues).some((items: any) => items.length)) throw Error(`Cannot verify. Missing artifacts: ${issues.missingArtifacts.join(', ')}. Failed/missing/stale checks: ${issues.pendingChecks.join(', ')}. Missing outputs: ${issues.missingOutputs.join(', ')}. Incomplete steps: ${issues.incompleteSteps.join(', ')}. Unverified requirements: ${issues.unverifiedRequirements.join('; ')}. Review: ${issues.review.join('; ')}`);
774
+ }
775
+ } else {
776
+ if (!params.limitations.length) throw Error('Blocked delivery requires an explicit limitation/reason');
777
+ try {
778
+ hash = fingerprint(ctx.cwd, ['.']);
779
+ } catch {
780
+ hash = 'unfingerprinted:scope_exceeded';
781
+ }
782
+ }
783
+ state.status = params.status;
784
+ const advisoryFailures = Object.entries(state.evidence || {}).filter(([, e]: any) => !e.passed).map(([id]) => id);
785
+ state.handoff = { ...params, verification, advisory, advisoryFailures, fingerprint: hash, at: new Date().toISOString(), evidence: structuredClone(state.evidence), reviewEvidence: state.reviewEvidence ? structuredClone(state.reviewEvidence) : null };
786
+ persist(ctx);
787
+ return text({ status: state.status, verification, advisory, advisoryFailures, report: join(directory(ctx), 'report.json'), note: advisory ? 'Informational/advisory finish: recorded checks are context, not a delivery guarantee. Distinguish them from unperformed manual/visual review.' : 'Evidence covers declared checks, not a guarantee of correctness. Distinguish automated evidence from unperformed manual/visual review.' });
788
+ }));
789
+ },
790
+ });
791
+ pi.on('agent_end', (event, ctx) => {
792
+ clearTimeout(budgetTimer);
793
+ if (ensureBudget(ctx)) return;
794
+ let release: (() => void) | undefined;
795
+ try {
796
+ release = lockWorkspace(ctx.cwd);
797
+ refreshState(ctx);
798
+ } catch { release?.(); return; } // Another operation/run owns continuation; never replay stale repairs.
799
+ try {
800
+ const last = [...event.messages].reverse().find((m: any) => m.role === 'assistant') as any;
801
+ let fresh = false, pending: string[] = [], failures: string[] = [];
802
+ try {
803
+ const hash = fingerprint(ctx.cwd, ['.']);
804
+ fresh = !!state && state.handoff?.fingerprint === hash;
805
+ if (state) {
806
+ pending = pendingChecks(state, hash);
807
+ failures = Object.entries(state.evidence).filter(([, e]: any) => !e.passed)
808
+ .map(([id, e]: any) => `check ${id} failed (exit ${e.code}, timedOut ${e.timedOut})${e.outputTail ? `; last output: ${String(e.outputTail).slice(-400)}` : ''}`);
809
+ }
810
+ } catch { /* explicit re-verification required */ }
811
+ if (!shouldContinue({ state, touched, nudges, stopReason: last?.stopReason, pendingMessages: ctx.hasPendingMessages(), fresh })) return;
812
+ if (ensureBudget(ctx, 'repair')) return;
813
+ nudges++;
814
+ if (state) persist(ctx);
815
+ const specifics = [...(pending.length ? [`pending checks: ${pending.join(', ')}`] : []), ...failures].join('\n');
816
+ pi.sendMessage({ customType: 'delivery-gate', display: true, content: `Delivery follow-up ${nudges}/2: implementation ended without current verified evidence.${specifics ? `\n${specifics}\n` : ''}Read the quoted output, repair the root cause it points to (do not rationalize a failing probe as an environment limitation without evidence), rerun the failing checks, then delivery_finish. If genuinely blocked, record status=blocked with specific limitations. Do not merely repeat a success claim.` }, { triggerTurn: true, deliverAs: 'followUp' });
817
+ } finally { release(); }
818
+ });
819
+ }