carthagent 1.0.7 → 1.1.0-beta.1

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 (294) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  6. package/docs/glyph-review/test-isolation-v1.md +26 -0
  7. package/docs/glyph-review/typecheck-base.log +1 -0
  8. package/docs/glyph-review/typecheck-v4.log +1 -0
  9. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  10. package/docs/help-dispatch-review.md +69 -0
  11. package/docs/launch-readiness-live-results.md +65 -0
  12. package/docs/legacy-cli.md +150 -0
  13. package/docs/live-theme-plugin-api.md +18 -0
  14. package/docs/native-cli.md +102 -0
  15. package/docs/native-compatibility.md +16 -0
  16. package/docs/native-daily-quests.md +132 -0
  17. package/docs/native-glyph-pty.md +59 -0
  18. package/docs/native-manual-testing.md +336 -0
  19. package/docs/native-slash-acceptance-handoff.md +49 -0
  20. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  21. package/docs/native-terminal-performance.md +310 -0
  22. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  23. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  24. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  35. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  36. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  37. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  38. package/docs/native-terminal-semantics.md +89 -0
  39. package/docs/native-upgrade.md +175 -0
  40. package/docs/opencode-composed-source-review.md +89 -0
  41. package/docs/opencode-migration.md +225 -9
  42. package/docs/opencode-owned-runtime.md +241 -0
  43. package/docs/opencode-runtime.md +12 -0
  44. package/docs/opencode-theme-provider-proof/README.md +55 -0
  45. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  46. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  47. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  48. package/docs/owned-host-controls-proof.md +39 -0
  49. package/docs/owned-runtime-release-manifest.md +42 -0
  50. package/docs/owned-visual-clear-proof.md +40 -0
  51. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  52. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  53. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  54. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  61. package/docs/release-notes-beta.1.md +60 -0
  62. package/docs/release-notes-native-draft.md +73 -0
  63. package/docs/release-notes-preview.10.md +121 -0
  64. package/docs/release-notes-preview.11.md +76 -0
  65. package/docs/release-notes-preview.12.md +55 -0
  66. package/docs/release-notes-preview.13.md +68 -0
  67. package/docs/release-notes-preview.9.md +130 -0
  68. package/docs/source-preview.md +120 -0
  69. package/evaluate.mjs +28 -1
  70. package/lib/budget.mjs +1 -1
  71. package/lib/codegraph.mjs +1 -1
  72. package/lib/delivery-review.mjs +19 -6
  73. package/lib/delivery.mjs +3 -2
  74. package/lib/environment.mjs +8 -0
  75. package/lib/guidance.mjs +1 -1
  76. package/lib/opencode/cli-runtime.mjs +1171 -0
  77. package/lib/opencode/cli.mjs +252 -0
  78. package/lib/opencode/credentials.mjs +63 -8
  79. package/lib/opencode/demo.mjs +53 -0
  80. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  81. package/lib/opencode/evaluation.mjs +32 -12
  82. package/lib/opencode/headless.mjs +22 -5
  83. package/lib/opencode/provider-policy.mjs +23 -0
  84. package/lib/opencode/review.mjs +37 -16
  85. package/lib/opencode/runtime-artifact.mjs +335 -0
  86. package/lib/opencode/runtime-github-transport.mjs +121 -0
  87. package/lib/opencode/runtime-release.json +36 -0
  88. package/lib/opencode/runtime.mjs +299 -20
  89. package/lib/opencode/service.mjs +77 -35
  90. package/lib/opencode/session-options.mjs +6 -2
  91. package/lib/opencode/update.mjs +222 -0
  92. package/lib/opencode/workspace.mjs +1 -1
  93. package/lib/planning-advice.mjs +2 -2
  94. package/lib/providers/experiential.mjs +2 -2
  95. package/lib/tui/framing.mjs +1 -80
  96. package/lib/update.mjs +10 -1
  97. package/package.json +33 -7
  98. package/plugins/carthagent/README.md +23 -0
  99. package/plugins/carthagent/src/bootstrap.ts +1 -1
  100. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  101. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  102. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  103. package/plugins/carthagent/src/index.ts +71 -24
  104. package/plugins/carthagent/src/rpc.ts +51 -6
  105. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  106. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  107. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  108. package/plugins/carthagent/src/server/delivery.ts +227 -112
  109. package/plugins/carthagent/src/server/framing.mjs +80 -0
  110. package/plugins/carthagent/src/server/policy.ts +1 -1
  111. package/plugins/carthagent/src/server/providers.ts +59 -31
  112. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  113. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  114. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  115. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  116. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  117. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  118. package/plugins/carthagent/src/testing.ts +5 -0
  119. package/plugins/carthagent/src/tui.tsx +496 -0
  120. package/plugins/carthagent/src/ui/client.ts +127 -0
  121. package/plugins/carthagent/src/ui/commands.ts +70 -0
  122. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  123. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  124. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  125. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  126. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  127. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  128. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  129. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  130. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  131. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  132. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  133. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  134. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  135. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  136. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  137. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  138. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  139. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  140. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  141. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  142. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  143. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  144. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  145. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  146. package/plugins/carthagent/tests/commands.test.ts +107 -0
  147. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  148. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  149. package/plugins/carthagent/themes/carthage.json +67 -0
  150. package/plugins/carthagent/tui.ts +26 -1
  151. package/script/ctg/build-owned-runtime.sh +166 -0
  152. package/tests/budget-engine.test.mjs +4 -3
  153. package/tests/cli.test.mjs +52 -96
  154. package/tests/daily-evidence.test.mjs +62 -0
  155. package/tests/delivery-review.test.mjs +57 -2
  156. package/tests/delivery.test.mjs +22 -1
  157. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  158. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  159. package/tests/installed-runtime-contract.test.mjs +43 -0
  160. package/tests/native-cli-host-smoke.mjs +457 -0
  161. package/tests/native-daily-editor-focus.test.mjs +213 -0
  162. package/tests/native-daily-editor.test.mjs +43 -0
  163. package/tests/native-daily-quests-layout.test.mjs +55 -0
  164. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  165. package/tests/native-daily-quests-ui.test.mjs +778 -0
  166. package/tests/native-daily-source.test.mjs +525 -0
  167. package/tests/native-demo-ui.test.mjs +48 -0
  168. package/tests/native-evaluation-admission.test.mjs +24 -0
  169. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  170. package/tests/native-fixture-runtime.test.mjs +111 -0
  171. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  172. package/tests/native-glyph-pty.test.mjs +107 -0
  173. package/tests/native-manual-projects.test.mjs +170 -0
  174. package/tests/native-model-command.test.mjs +67 -0
  175. package/tests/native-owned-runtime-env.test.mjs +140 -0
  176. package/tests/native-paired-guards.test.mjs +464 -0
  177. package/tests/native-provider-policy.test.mjs +31 -0
  178. package/tests/native-pty-invocation.test.mjs +114 -0
  179. package/tests/native-pty-runtime.test.mjs +45 -0
  180. package/tests/native-resume-session.test.mjs +46 -0
  181. package/tests/native-review-consumer.test.mjs +286 -0
  182. package/tests/native-runtime-identity.test.mjs +214 -0
  183. package/tests/native-select-mouse.test.mjs +23 -0
  184. package/tests/native-ship-guidance.test.mjs +16 -0
  185. package/tests/native-ship-parity.test.mjs +526 -0
  186. package/tests/native-ship-plan.test.mjs +85 -0
  187. package/tests/native-ship-rpc.test.mjs +71 -0
  188. package/tests/native-ui-commands.test.mjs +6 -0
  189. package/tests/native-voyages-layout.test.mjs +28 -0
  190. package/tests/native-voyages-navigation.test.mjs +169 -0
  191. package/tests/native-voyages-rpc.test.mjs +397 -0
  192. package/tests/native-voyages-ui.test.mjs +307 -0
  193. package/tests/opencode-atomic-write.test.mjs +55 -0
  194. package/tests/opencode-cli.test.mjs +1312 -0
  195. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  196. package/tests/opencode-delivery.test.mjs +16 -0
  197. package/tests/opencode-providers.test.mjs +245 -18
  198. package/tests/opencode-runtime.test.mjs +597 -8
  199. package/tests/opencode-session-controls.test.mjs +15 -0
  200. package/tests/opencode-ship-rpc.test.mjs +71 -0
  201. package/tests/opencode-update.test.mjs +475 -0
  202. package/tests/opencode-workspace.test.mjs +51 -2
  203. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  204. package/tests/release-workflow.test.mjs +46 -0
  205. package/tests/runtime-artifact.test.mjs +269 -0
  206. package/tests/runtime-development-trust.test.mjs +231 -0
  207. package/tests/runtime-github-transport.test.mjs +132 -0
  208. package/tests/runtime-token-env.test.mjs +23 -0
  209. package/tests/server.test.mjs +3 -1
  210. package/tests/ship-telemetry.test.mjs +1 -1
  211. package/tests/source-preview.test.mjs +253 -0
  212. package/tests/test_native_terminal_probe.py +76 -0
  213. package/tests/test_native_terminal_semantics.py +232 -0
  214. package/tests/update.test.mjs +42 -5
  215. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  216. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  217. package/tests/workspace-live.test.mjs +127 -0
  218. package/tools/benchmark/diagnose-request.mjs +59 -36
  219. package/tools/benchmark/eval-common.mjs +3 -3
  220. package/tools/benchmark/evaluators.test.mjs +4 -4
  221. package/tools/benchmark/guard.test.mjs +35 -2
  222. package/tools/benchmark/guard.ts +20 -7
  223. package/tools/benchmark/planning-startup.test.mjs +30 -34
  224. package/tools/benchmark/runner.mjs +12 -7
  225. package/tools/benchmark/runner.test.mjs +23 -7
  226. package/tools/benchmark/upstream-error.test.mjs +16 -2
  227. package/tools/generate-owned-runtime-release.mjs +217 -0
  228. package/tools/installed-runtime-contract.mjs +16 -0
  229. package/tools/manual-runtime-manifest.mjs +39 -0
  230. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  231. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  232. package/tools/native-fixture.mjs +199 -40
  233. package/tools/native-glyph-helpers.py +127 -0
  234. package/tools/native-glyph-pty.py +144 -0
  235. package/tools/native-manual-projects.mjs +259 -0
  236. package/tools/native-manual.mjs +338 -0
  237. package/tools/native-model-picker-acceptance.py +306 -0
  238. package/tools/native-owned-runtime-env.sh +47 -0
  239. package/tools/native-paired-guards.mjs +282 -0
  240. package/tools/native-paired-manifest.mjs +72 -0
  241. package/tools/native-paired-orchestration.sh +51 -0
  242. package/tools/native-plugin-readiness.mjs +33 -0
  243. package/tools/native-plugin-readiness.test.mjs +53 -0
  244. package/tools/native-pty-invocation.mjs +26 -0
  245. package/tools/native-pty-runtime.mjs +83 -0
  246. package/tools/native-pty-smoke.py +147 -24
  247. package/tools/native-public-repo-shell-result.mjs +13 -0
  248. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  249. package/tools/native-runtime-identity.mjs +201 -0
  250. package/tools/native-slash-acceptance-env.py +38 -0
  251. package/tools/native-terminal-semantics.py +876 -0
  252. package/tools/owned-shell-observer/index.ts +16 -0
  253. package/tools/owned-shell-observer/package.json +1 -0
  254. package/tools/owned-visual-clear-env.py +48 -0
  255. package/tools/owned-visual-clear-model.py +21 -0
  256. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  257. package/tools/prepare-native-manual.mjs +122 -0
  258. package/tools/run-native-terminal-paired.sh +403 -0
  259. package/tools/runtime-token-env.mjs +8 -0
  260. package/tools/seed-native-switch-fixture.mjs +79 -0
  261. package/tools/test-native-controls.mjs +3 -1
  262. package/tools/test-native-daily-quests.mjs +65 -0
  263. package/tools/test-native-default-cli.mjs +106 -0
  264. package/tools/test-native-delivery-host.mjs +102 -20
  265. package/tools/test-native-evaluation.mjs +7 -3
  266. package/tools/test-native-glyph-pty.mjs +94 -0
  267. package/tools/test-native-host.mjs +87 -63
  268. package/tools/test-native-outcomes.mjs +3 -1
  269. package/tools/test-native-package.mjs +206 -38
  270. package/tools/test-native-performance.mjs +26 -0
  271. package/tools/test-native-product.mjs +90 -0
  272. package/tools/test-native-pty.mjs +34 -9
  273. package/tools/test-native-public-repo.mjs +224 -0
  274. package/tools/test-native-refresh-reentry.mjs +106 -0
  275. package/tools/test-native-report.mjs +27 -20
  276. package/tools/test-native-review.mjs +75 -0
  277. package/tools/test-native-runtime.mjs +120 -0
  278. package/tools/test-native-ship-models.mjs +176 -0
  279. package/tools/test-native-ship-walkthrough.mjs +96 -0
  280. package/tools/test-native-slash-commands.mjs +125 -0
  281. package/tools/test-native-slash-commands.py +242 -0
  282. package/tools/test-native-swe-host.mjs +224 -55
  283. package/tools/test-native-tasks.mjs +77 -0
  284. package/tools/test-native-terminal-performance.py +887 -0
  285. package/tools/test-native-terminal-readiness.sh +85 -0
  286. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  287. package/tools/test-native-visual-clear-installed.mjs +48 -0
  288. package/tools/test-owned-host-controls.mjs +238 -0
  289. package/tools/test-owned-opencode-runtime.mjs +35 -0
  290. package/tools/test-owned-visual-clear-env.py +120 -0
  291. package/tools/test-owned-visual-clear.py +373 -0
  292. package/tools/verify-draft-runtime-gate.mjs +138 -0
  293. package/tools/verify-owned-runtime-release.mjs +114 -0
  294. package/tools/verify-release-version.mjs +21 -1
@@ -1,25 +1,33 @@
1
1
  import { randomUUID } from "node:crypto"
2
2
  import type { Context } from "@opencode/plugin/promise/plugin"
3
- import type { ToolContext } from "@opencode/plugin/promise/tool"
3
+ import { Error as NativeToolError, type ToolContext } from "@opencode/plugin/promise/tool"
4
4
  import type { Tool } from "@opencode/schema/tool"
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
5
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
6
6
  import { basename, join, resolve } from "node:path"
7
- import { DELIVERY_INVARIANTS, TEST_INFRASTRUCTURE_GUIDANCE, deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
8
- import { deliveryRecovery, designReceipt, recoveryMessages } from "../../../../lib/delivery-context.mjs"
7
+ import { deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
8
+ import { contextMetrics, deliveryRecovery, designReceipt, recoveryMessages } from "../../../../lib/delivery-context.mjs"
9
9
  import { validatePlanning, inspectPlanning, recordPlanReview, approvePlanning, planningStatus, startImplementation } from "../../../../lib/planning.mjs"
10
- import { PLANNING_START, planningGuide, planningNext } from "../../../../lib/planning-guide.mjs"
10
+ import { planningGuide, planningNext } from "../../../../lib/planning-guide.mjs"
11
11
  import { newDiscovery, restoreDiscovery, discoveryCheckpoint, discoveryEvent } from "../../../../lib/planning-discovery.mjs"
12
12
  import { budgetLimits, newBudget, budgetReason, budgetSnapshot } from "../../../../lib/budget.mjs"
13
- import { validatePlan, fingerprint, runCommand, pendingChecks, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, mergePlanPatch, completionIssues, planSteps, pendingRequirements, planD2WithProgress, evidenceIdentity } from "../../../../lib/delivery.mjs"
13
+ import { validatePlan, fingerprint, runCommand, pendingChecks, shouldContinue, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, mergePlanPatch, completionIssues, planSteps, pendingRequirements, planD2WithProgress, evidenceIdentity } from "../../../../lib/delivery.mjs"
14
14
  import { captureDeliveryReview, recordDeliveryReview } from "../../../../lib/delivery-review.mjs"
15
- import { formatGuidance, loadGuidanceProfiles, routeGuidance } from "../../../../lib/guidance.mjs"
15
+ import { loadGuidanceProfiles, routeGuidance } from "../../../../lib/guidance.mjs"
16
16
  import { saveReport, reconcileReport } from "../../../../lib/reports.mjs"
17
17
  import { lockWorkspace, selectReport, assertActiveReport } from "../../../../lib/workspace.mjs"
18
- import { enforceToolPolicy, policyDescription } from "./policy.ts"
18
+ import { enforceToolPolicy, requiresPlanApproval } from "./policy.ts"
19
19
  import { effectiveSessionOptions } from "../../../../lib/opencode/session-options.mjs"
20
+ import { archiveContext } from "../../../../lib/context-archive.mjs"
21
+ import { probeDiagnostics } from "../../../../lib/probe-diagnostics.mjs"
22
+ import { configuredAdvisor, inspectAdvice } from "../../../../lib/planning-advice.mjs"
23
+ import { createShipJev } from "../../../../lib/ship-jev.mjs"
24
+ import { cloudGatewayUrl } from "../../../../lib/providers/experiential.mjs"
25
+ import { isShipAgent } from "./ship-agents.ts"
26
+ import { originalTask, shipGuidance } from "./ship-guidance.ts"
27
+ import { archiveDeliveryExchanges, localToolName, normalizeDesignReview, shellProtection } from "./ship-context.mjs"
20
28
 
21
29
  /* eslint-disable @typescript-eslint/no-explicit-any -- Existing workflow snapshots mirror evolving JS domain schemas. */
22
- type SessionState = { state: any; discovery: any; nudges: number; touched: boolean; budget: any; required: any[]; configError?: string; extraGuidance?: string; initialFiles: string[]; lastPrompt?: string; guidance?: string; writeCounts?: Record<string, number>; outcome?: string; cwd?: string }
30
+ type SessionState = { state: any; discovery: any; nudges: number; touched: boolean; budget: any; required: any[]; configError?: string; extraGuidance?: string; initialFiles: string[]; lastPrompt?: string; guidance?: string; writeCounts?: Record<string, number>; outcome?: string; cwd?: string; guidanceProfiles?: string[]; probeState?: { failures: number; family: string | null }; statusPoll?: { key: string; count: number }; contextBytes?: number; recovery?: Record<string, any>; initialFilesCaptured?: boolean; model?: { providerID: string; id: string } }
23
31
  type DeliveryOptions = Record<string, unknown>
24
32
  type Schema = Record<string, unknown>
25
33
  type DeliveryInput = Record<string, any>
@@ -35,7 +43,7 @@ const short = S()
35
43
  const steps = A({ anyOf: [short, O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, title: short, kind: { type: "string", enum: ["work", "regression"] }, dependsOn: A(short, 40), checks: A(short, 24) }, ["id", "title"])] }, 40)
36
44
  const checks = A(O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, kind: { type: "string", enum: ["test", "runtime", "static"] }, argv: A(short, 80), timeoutSeconds: { type: "integer", minimum: 1, maximum: 300 } }), 24)
37
45
  const acceptance = A(O({ requirement: short, checks: A(short, 24) }), 40)
38
- const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(short, 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
46
+ const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(S("Relative file path that must exist at completion, e.g. invoice.mjs. Not a prose deliverable or command result; put test commands in checks. Omit outputs when no files are promised."), 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
39
47
 
40
48
  function sessionKey(sessionID: string) { return `session/${sessionID}` }
41
49
 
@@ -97,7 +105,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
97
105
  try { data.extraGuidance = readFileSync(resolve(cwd, opts.context), "utf8") }
98
106
  catch (error) { data.configError = `Invalid delivery context: ${error}` }
99
107
  }
100
- if (opts.protectExisting) data.initialFiles = readdirSync(cwd, { withFileTypes: true }).filter(e => e.isFile()).map(e => resolve(cwd, e.name))
108
+ if (opts.protectExisting && !data.initialFilesCaptured) {
109
+ data.initialFiles = readdirSync(cwd, { withFileTypes: true }).filter(e => e.isFile()).map(e => resolve(cwd, e.name))
110
+ data.initialFilesCaptured = true
111
+ }
101
112
  }
102
113
  if (data.state && data.required.length) {
103
114
  for (const check of data.required) {
@@ -106,7 +117,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
106
117
  }
107
118
  data.state.plan = bindRequiredChecks(data.state.plan, data.required)
108
119
  }
109
- return { session, cwd, data, options: opts }
120
+ return { session, cwd, data, options: opts, active: Boolean(opts.delivery) && isShipAgent(session.agent) }
110
121
  }
111
122
  function runDirectory(cwd: string, sessionID: string, runId: string) { return join(cwd, ".harness", sessionID, runId) }
112
123
  async function persist(cwd: string, sessionID: string, data: SessionState) {
@@ -138,23 +149,30 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
138
149
  } finally { release() }
139
150
  })
140
151
  }
152
+ // The code-mode catalog drops the longest signatures first; pin the plan entry point so it is always listed.
141
153
  const add = (editor: Parameters<Parameters<Context["tool"]["transform"]>[0]>[0], name: string, description: string, input: Schema, execute: (input: DeliveryInput, toolContext: RegisteredToolContext) => Promise<{ content: string }>) => editor.add({
142
- name, description, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
154
+ name, description: `${description} Returns text; in code mode use JSON.parse(result) for JSON responses before reading fields. Plain-text messages are not JSON.`, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
143
155
  const value = rawValue as DeliveryInput
144
- const { options: opts } = await scope(tool.sessionID)
145
- if (!opts.delivery) throw Error("Carthagent delivery tools are disabled for this session.")
146
- return execute(value, tool)
147
- }, options: { namespace: "carthagent", codemode: true },
156
+ const { active } = await scope(tool.sessionID)
157
+ if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
158
+ try {
159
+ const result = await execute(value, tool)
160
+ const { data } = await scope(tool.sessionID)
161
+ if (!data.state) return result
162
+ return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
163
+ }
164
+ catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
165
+ }, options: { namespace: "carthagent", codemode: true, ...(name === "delivery_plan" ? { pinned: true } : {}) },
148
166
  })
149
167
 
150
168
  registrations.push(await ctx.tool.transform(editor => {
151
169
  editor.namespace?.({ name: "carthagent", description: "Evidence-driven CTG delivery workflow tools" })
152
- add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
170
+ add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Outputs are relative file paths, not prose or test results. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
153
171
  if (data.configError) throw Error(data.configError)
154
172
  const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
155
173
  validateRevision(data.state, plan)
156
174
  if (data.state && !["verified", "blocked"].includes(data.state.status)) data.state = revisePlan(data.state, plan, { cwd, hash: fingerprint(cwd, ["."]), reason: "Active plan replaced via delivery_plan" })
157
- else data.state = { version: 1, workflowMode: opts.workflow === "strict" ? "strict" : "standard", reviewRequired: opts.workflow !== "strict", runId: randomUUID(), revision: (data.state?.revision || 0) + 1, plan, evidence: {}, status: "implementing", createdAt: new Date().toISOString(), stepStatus: {} }
175
+ else data.state = { version: 1, workflowMode: opts.workflow === "strict" ? "strict" : "standard", reviewRequired: opts.workflow !== "strict", runId: randomUUID(), revision: (data.state?.revision || 0) + 1, plan, evidence: {}, status: "implementing", createdAt: new Date().toISOString(), stepStatus: {}, guidanceProfiles: data.guidanceProfiles || [] }
158
176
  data.discovery = newDiscovery(); const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
159
177
  mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
160
178
  await persist(cwd, tool.sessionID, data)
@@ -172,23 +190,51 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
172
190
  await persist(cwd, tool.sessionID, data)
173
191
  return text({ revision: data.state.revision, pendingChecks: pendingChecks(data.state, hash), unverifiedRequirements: pendingRequirements(data.state, hash), next: data.state.workflowMode === "standard" ? "Continue implementation; changed checks require fresh evidence." : "Revision relocks implementation; validate and re-approve." })
174
192
  }))
175
- add(editor, "delivery_design", "Strict workflow design gate. Guide, validate, inspect, adversarially review every scenario, then approve. Review is model-authored, not independent proof.", O({ action: { type: "string", enum: ["guide", "validate", "inspect", "review", "approve"] }, captureId: short, review: { type: "object", additionalProperties: true } }, ["action"]), async (input, tool) => {
193
+ add(editor, "delivery_design", "Strict workflow design gate. Guide, validate, inspect, adversarially review every scenario, then approve. Review is model-authored, not independent proof.", O({ action: { type: "string", enum: ["guide", "validate", "inspect", "review", "approve"] }, captureId: short, review: { type: "object", additionalProperties: true }, challenges: A(short, 40), walkthroughs: A({ type: "object", additionalProperties: true }, 80), findings: A({ type: "object", additionalProperties: true }, 80), limitations: A(short) }, ["action"]), async (input, tool) => {
176
194
  if (input.action === "guide") return text(planningGuide())
177
- return operate(tool.sessionID, async ({ cwd, data }) => {
195
+ return operate(tool.sessionID, async ({ session, cwd, data, options: opts }) => {
178
196
  if (!data.state) throw Error("Call delivery_plan first")
179
197
  const hash = fingerprint(cwd, ["."]); let result
180
198
  if (input.action === "validate") result = validatePlanning(data.state, hash)
181
199
  else if (input.action === "inspect") result = inspectPlanning(data.state, hash)
182
- else if (input.action === "review") result = recordPlanReview(data.state, hash, { ...input.review, captureId: input.review?.captureId ?? input.captureId })
200
+ else if (input.action === "review") result = recordPlanReview(data.state, hash, normalizeDesignReview(input))
183
201
  else if (input.action === "approve") result = approvePlanning(data.state, hash)
184
202
  else throw Error("Unknown delivery_design action")
203
+ let advice
204
+ if (input.action === "inspect") {
205
+ const ship = (session.model ?? data.model)?.providerID === "experiential-labs"
206
+ const enabled = opts.jev === undefined ? ship : opts.jev === true
207
+ const createAdvisor = () => {
208
+ if (opts.jevProvider === "ship" || (ship && !opts.jevProvider)) {
209
+ return { classify: async (plan: unknown) => {
210
+ const active = await ctx.integration.connection.active("experiential-labs")
211
+ if (!active || active.type !== "credential" || active.method !== "oauth") throw Error("Ship OAuth connection required")
212
+ const credential = await ctx.integration.connection.resolve(active)
213
+ if (credential?.type !== "oauth" || credential.methodID !== "carthagent-device" || !credential.access) throw Error("Ship OAuth credential required")
214
+ return createShipJev({ fetchImpl: globalThis.fetch, ...{ accessToken: credential.access, baseUrl: cloudGatewayUrl(), signal: tool.signal } }).classify(plan)
215
+ } }
216
+ }
217
+ return configuredAdvisor({ ...{ enabled, provider: String(opts.jevProvider || "openrouter"), signal: tool.signal } })
218
+ }
219
+ advice = await inspectAdvice(data.state, result, { ...{ enabled, createAdvisor, persist: () => persist(cwd, tool.sessionID, data) } })
220
+ }
221
+ let receipt = designReceipt(input.action, result)
222
+ if (input.action === "inspect" && JSON.stringify(result).length > 16000) {
223
+ try {
224
+ const archive = archiveContext(cwd, runDirectory(cwd, tool.sessionID, data.state.runId), result)
225
+ receipt = { ...result, plan: undefined, capturePath: archive.path, captureDigest: archive.digest, captureBytes: archive.bytes, instruction: "Read the complete capturePath before reviewing. This is not an approval." }
226
+ } catch { /* Retain the original capture if archival fails. */ }
227
+ }
185
228
  await persist(cwd, tool.sessionID, data)
186
- return text({ result: designReceipt(input.action, result), ...(input.action === "inspect" ? { captureId: result.id } : {}), ...(input.action === "validate" ? planningNext(data.state, result) : {}), planningStatus: planningStatus(data.state, hash) })
229
+ return text({ result: receipt, ...(advice ? { advice } : {}), ...(input.action === "inspect" ? { captureId: result.id } : {}), ...(input.action === "validate" ? planningNext(data.state, result) : {}), planningStatus: planningStatus(data.state, hash) })
187
230
  })
188
231
  })
189
232
  add(editor, "delivery_status", "Show delivery state, stale/failed checks and next action. Call finish instead of polling when readyToFinish is true.", empty, async (_input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
190
233
  if (!data.state) return text("No delivery contract.")
191
- const dir = runDirectory(cwd, tool.sessionID, data.state.runId); const hash = fingerprint(cwd, ["."])
234
+ const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
235
+ let hash: string
236
+ try { hash = fingerprint(cwd, ["."]) }
237
+ catch (error) { return text({ ...deliveryRecovery(data.state, { report: join(dir, "report.json") }), pendingChecks: [`[scope error: ${error}]`], readyToFinish: false, next: 'Evidence scope could not be established. Call delivery_finish with status="blocked" and specific limitations.' }) }
192
238
  const issues = completionIssues(data.state, cwd, hash); const readyToFinish = Object.values(issues).every(items => !items.length)
193
239
  return text({ ...deliveryRecovery(data.state, { report: join(dir, "report.json") }), pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish, next: readyToFinish ? "Call delivery_finish; do not poll again." : "Run declared checks, complete steps/outputs, and resolve outstanding requirements." })
194
240
  }))
@@ -212,40 +258,61 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
212
258
  for (const check of selected) {
213
259
  if (tool.signal.aborted) throw Error("Check cancelled before execution")
214
260
  const identity = evidenceIdentity(data.state, check); data.state.status = "verifying"; data.state.verificationStarted = true; delete data.state.evidence[check.id]
215
- const before = fingerprint(cwd, ["."]); await persist(cwd, tool.sessionID, data)
261
+ let before: string
262
+ try { before = fingerprint(cwd, ["."]) }
263
+ catch (error) {
264
+ data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: 1, outputTail: `Evidence scope error: ${error}` }
265
+ await persist(cwd, tool.sessionID, data)
266
+ throw Error(`Evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations rather than retrying.`)
267
+ }
268
+ await persist(cwd, tool.sessionID, data)
269
+ await tool.progress?.({ title: `Running ${check.id}`, description: JSON.stringify(check.argv) })
216
270
  const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
217
271
  const result = await runCommand(check.argv, { cwd, timeoutSeconds: check.timeoutSeconds, signal: tool.signal, logPath: join(dir, `${check.id}-${randomUUID()}.log`) } as Parameters<typeof runCommand>[1])
218
- const after = fingerprint(cwd, ["."]); const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
272
+ let after: string
273
+ try { after = fingerprint(cwd, ["."]) }
274
+ catch (error) {
275
+ data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: result.code, logPath: result.logPath, outputTail: `Post-check evidence scope error: ${error}\n${result.output.slice(-800)}` }
276
+ await persist(cwd, tool.sessionID, data)
277
+ throw Error(`Post-check evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations.`)
278
+ }
279
+ const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
219
280
  data.state.evidence[check.id] = { passed, ...identity, executedRevision: data.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) }
281
+ // The first execution in a run is its baseline; later receipts replace evidence only.
282
+ const baselines = (data.state.baselines ??= {}) as Record<string, unknown>
283
+ if (!baselines[check.id]) baselines[check.id] = { passed, code: result.code, checkDigest: identity.checkDigest, fingerprint: after, at: new Date().toISOString() }
220
284
  results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
221
285
  await persist(cwd, tool.sessionID, data)
222
286
  }
223
287
  const failures = results.filter(r => !r.passed && verification === "required")
224
288
  if (failures.length) throw Error(JSON.stringify({ failures, next: "Repair actual failures; do not weaken obligations." }))
225
289
  const hash = fingerprint(cwd, ["."]); const issues = completionIssues(data.state, cwd, hash)
226
- return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length) })
290
+ return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length), next: "Complete pending steps, review the evidence, then call delivery_finish." })
227
291
  }))
228
- add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
292
+ add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", properties: { requirement: { type: "string", description: "Exact acceptance requirement text" }, assertions: { anyOf: [{ type: "string" }, A({ type: "string" }, 32)], description: "Concrete assertions the linked checks make" }, assertion: { type: "string", description: "Alias for a single assertion" }, status: { type: "string", enum: ["demonstrated", "unverified"] } }, required: ["requirement"], additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
229
293
  if (!data.state) throw Error("Call delivery_plan first")
230
294
  const result = input.action === "inspect" ? await captureDeliveryReview(data.state, cwd, runDirectory(cwd, tool.sessionID, data.state.runId), tool.signal) : input.action === "record" ? recordDeliveryReview(data.state, cwd, input) : (() => { throw Error("Review action must be inspect or record") })()
231
295
  await persist(cwd, tool.sessionID, data); return text(result)
232
296
  }))
233
- add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
297
+ add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, change: { ...short, description: "One-paragraph summary of what changed and why" }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
234
298
  if (!data.state) throw Error("Call delivery_plan first")
235
299
  if (data.configError) throw Error(data.configError)
236
300
  const verification = verificationMode(data.state.plan); let hash: string | null = null
237
301
  if (input.status === "verified") {
238
- hash = fingerprint(cwd, ["."])
239
302
  if (verification === "required") {
303
+ hash = fingerprint(cwd, ["."])
240
304
  const issues = completionIssues(data.state, cwd, hash)
241
305
  if (Object.values(issues).some(items => items.length)) throw Error(`Cannot verify: ${JSON.stringify(issues)}`)
242
- }
306
+ } else { try { hash = fingerprint(cwd, ["."]) } catch { hash = null } }
243
307
  } else {
244
308
  if (!input.limitations.length) throw Error("Blocked delivery requires an explicit limitation/reason")
245
309
  try { hash = fingerprint(cwd, ["."]) } catch { hash = "unfingerprinted:scope_exceeded" }
246
310
  }
247
- data.state.status = input.status; data.state.handoff = { ...input, verification, advisory: verification !== "required", fingerprint: hash, at: new Date().toISOString(), evidence: structuredClone(data.state.evidence), reviewEvidence: data.state.reviewEvidence || null }
248
- await persist(cwd, tool.sessionID, data); return text({ status: data.state.status, verification, report: join(runDirectory(cwd, tool.sessionID, data.state.runId), "report.json") })
311
+ const advisory = verification !== "required"
312
+ const advisoryFailures = Object.entries(data.state.evidence).filter(([, value]) => !(value as { passed: boolean }).passed).map(([id]) => id)
313
+ data.state.status = input.status; data.state.handoff = { ...input, verification, advisory, advisoryFailures, fingerprint: hash, at: new Date().toISOString(), evidence: structuredClone(data.state.evidence), reviewEvidence: structuredClone(data.state.reviewEvidence || null) }
314
+ await persist(cwd, tool.sessionID, data)
315
+ return text({ status: data.state.status, verification, advisory, advisoryFailures, 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.", report: join(runDirectory(cwd, tool.sessionID, data.state.runId), "report.json") })
249
316
  }))
250
317
  }))
251
318
 
@@ -256,35 +323,50 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
256
323
  // share the outer call ID/message ID while still being distinct invocations.
257
324
  const callKey = `${event.sessionID}\0${event.messageID}\0${event.id}\0${event.tool}`
258
325
  if (admittedCalls.has(callKey)) return
259
- const { cwd, data, options: opts } = await scope(event.sessionID)
260
- if (!opts.delivery && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
326
+ const { cwd, data, options: opts, active: selected } = await scope(event.sessionID)
327
+ const active = selected && isShipAgent(event.agent)
328
+ const toolName = localToolName(event.tool)
329
+ if (toolName.startsWith("delivery_") && !active) throw Error("Delivery tools require Ship mode.")
330
+ if (!active && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
261
331
  let persistenceAttempted = false
262
332
  try {
263
333
  const nativeShell = ["bash", "shell", "powershell"].includes(event.tool)
264
334
  const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {}
265
- if (opts.delivery && event.tool !== "execute" && requiresImplementationTool(event.tool)) {
266
- if (!data.state) throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}).`)
335
+ if (active && toolName === "delivery_status" && data.state) {
336
+ let source
337
+ try { source = fingerprint(cwd, ["."]) } catch (error) { source = String(error) }
338
+ const key = JSON.stringify([data.state.runId, data.state.revision, source, data.state.evidence, data.state.stepStatus, data.state.reviewEvidence, data.state.planning, data.state.status])
339
+ data.statusPoll = { key, count: data.statusPoll?.key === key ? data.statusPoll.count + 1 : 1 }
340
+ if (data.statusPoll.count > 3) throw Error("Repeated unchanged delivery_status polling is blocked. Execute the next action or finish instead.")
341
+ } else if (active && (toolName.startsWith("delivery_") || ["write", "edit", "patch"].includes(toolName))) data.statusPoll = undefined
342
+ if (active && event.tool !== "execute" && requiresImplementationTool(toolName)) {
343
+ if (!data.state) {
344
+ data.discovery = discoveryEvent(data.discovery, "blocked")
345
+ throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}). ${discoveryCheckpoint(data.discovery) || ""}`)
346
+ }
267
347
  const release = lockWorkspace(cwd)
268
348
  try {
269
349
  const dir = runDirectory(cwd, event.sessionID, data.state.runId)
270
350
  assertActiveReport(cwd, join(dir, "report.json"))
271
351
  data.state = reconcileReport(join(dir, "report.json"), data.state)
272
- enforceToolPolicy(event.tool, data.state, cwd)
352
+ enforceToolPolicy(toolName, data.state, cwd)
273
353
  await persist(cwd, event.sessionID, data)
274
354
  } finally { release() }
275
355
  }
276
- if ((event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
356
+ if (active && (event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
277
357
  const path = input.path.replaceAll("\\", "/"), cwdPath = cwd.replaceAll("\\", "/").replace(/^\/+/, ""), name = basename(cwd)
278
358
  if ([cwdPath, name].some(prefix => path === prefix || path.startsWith(`${prefix}/`))) throw Error("Path recreates working directory inside itself; use a workspace-relative product path.")
279
359
  if (event.tool === "write" && data.initialFiles.includes(resolve(cwd, input.path))) throw Error(`Preserve existing ${input.path}; use focused edits.`)
280
360
  }
281
361
  const cap = Number(opts.bashCap) || 0
282
- if (nativeShell) {
362
+ if (active && nativeShell) {
283
363
  const command = String(input.command ?? "")
284
- if (/\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(command)) throw Error("Broad process termination is prohibited; terminate only a captured child PID.")
285
- if (cap > 0 && (typeof input.timeout !== "number" || input.timeout > cap)) input.timeout = cap
364
+ const protection = shellProtection(command, opts.protectExisting ? data.initialFiles : [])
365
+ if (protection) throw Error(protection)
366
+ const capMilliseconds = cap * 1000
367
+ if (cap > 0 && (typeof input.timeout !== "number" || input.timeout === 0 || input.timeout > capMilliseconds)) input.timeout = capMilliseconds
286
368
  }
287
- if (["write", "edit", "patch"].includes(event.tool) && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
369
+ if (active && event.tool === "write" && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
288
370
  data.writeCounts ||= {}
289
371
  const path = String(input.path || "")
290
372
  const count = data.writeCounts[path] || 0
@@ -317,92 +399,115 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
317
399
  })))
318
400
 
319
401
  registrations.push(await ctx.tool.hook("execute.after", async event => serialized(event.sessionID, async () => {
320
- const { cwd, data } = await scope(event.sessionID)
321
- if (!sessionOptions(await ctx.session.get({ sessionID: event.sessionID })).delivery) {
322
- await ctx.storage.set(sessionKey(event.sessionID), data)
323
- return
324
- }
402
+ const { cwd, data, options: opts, active } = await scope(event.sessionID)
403
+ if (!active || !isShipAgent(event.agent)) return
404
+ const tool = localToolName(event.tool)
325
405
  const failed = event.status === "error"
326
- if (!failed && ["write", "edit"].includes(event.tool)) data.touched = true
327
- if (data.discovery.active && ["read", "ls", "find", "grep", "code_nav"].includes(event.tool)) data.discovery = discoveryEvent(data.discovery, "read")
328
- if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(event.tool)) {
406
+ if (!failed && ["write", "edit", "patch"].includes(tool)) data.touched = true
407
+ if (data.discovery.active && ["read", "ls", "glob", "find", "grep", "code_nav"].includes(tool)) data.discovery = discoveryEvent(data.discovery, "read")
408
+ const rawContent = event.status === "error" ? event.error.message : event.result?.content
409
+ const content = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent || []
410
+ const diagnostic = probeDiagnostics(data.probeState as Parameters<typeof probeDiagnostics>[0], { toolName: tool === "shell" ? "bash" : tool, isError: failed, content })
411
+ data.probeState = diagnostic.state
412
+ const cap = Math.max(0, Number(opts.toolOutputCap) || 0)
413
+ if (cap && event.status === "completed" && ["bash", "shell", "read", "grep", "glob", "find", "ls", "code_nav"].includes(tool)) {
414
+ event.result = { ...event.result, content: (typeof event.result.content === "string" ? [{ type: "text" as const, text: event.result.content }] : event.result?.content || []).map(part => part.type === "text" && part.text.length > cap ? { ...part, text: `${part.text.slice(-cap)}\n[output capped at ${cap} characters; request a targeted slice]` } : part) }
415
+ }
416
+ if (diagnostic.message) {
417
+ if (event.status === "completed") event.result = { ...event.result, content: [...(typeof event.result.content === "string" ? [{ type: "text" as const, text: event.result.content }] : event.result?.content || []), { type: "text", text: diagnostic.message }] }
418
+ else event.error = new NativeToolError({ message: `${event.error.message}\n\n${diagnostic.message}` })
419
+ }
420
+ if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(tool)) {
329
421
  const dir = runDirectory(cwd, event.sessionID, data.state.runId)
330
- if (existsSync(join(dir, "report.json"))) data.state = reconcileReport(join(dir, "report.json"), data.state)
422
+ const release = lockWorkspace(cwd)
423
+ try {
424
+ assertActiveReport(cwd, join(dir, "report.json"))
425
+ data.state = reconcileReport(join(dir, "report.json"), data.state)
426
+ writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
427
+ } finally { release() }
331
428
  }
332
- const { options: opts } = await scope(event.sessionID)
333
- const outputCap = Math.max(0, Number(opts.toolOutputCap) || 0)
334
- if (!failed && outputCap > 0 && event.result?.content) {
335
- if (!Array.isArray(event.result.content)) {
336
- await ctx.storage.set(sessionKey(event.sessionID), data)
337
- return
338
- }
339
- const content = event.result.content.map(item => {
340
- if (item.type !== "text" || item.text.length <= outputCap) return item
341
- const marker = `\n...[${item.text.length - outputCap} chars omitted; rerun a narrower command]...\n`
342
- const head = Math.floor(Math.max(0, outputCap - marker.length) / 3)
343
- const tail = Math.max(0, outputCap - marker.length - head)
344
- return { ...item, text: outputCap <= marker.length ? item.text.slice(0, outputCap) : item.text.slice(0, head) + marker + item.text.slice(-tail) }
345
- })
346
- event.result = { ...event.result, content }
429
+ if (event.status === "completed" && data.state && (tool === "execute" || tool.startsWith("delivery_"))) {
430
+ event.result = { ...event.result, metadata: { ...event.result.metadata, deliveryPlan: planSnapshot(data.state) } }
347
431
  }
348
432
  await ctx.storage.set(sessionKey(event.sessionID), data)
433
+ const delay = Math.min(30000, Math.max(0, Number(opts.turnDelayMs) || 0) * Math.max(1, Math.ceil((data.contextBytes || 0) / 4 / 50000)))
434
+ if (delay) await new Promise(resolve => setTimeout(resolve, delay))
349
435
  })))
350
436
 
351
437
  registrations.push(await ctx.shell.hook("create.before", async event => {
352
438
  // Session-scoped policy, limits, and budget were handled at execute.before.
353
439
  // This hook intentionally does not attribute commands: 2.0.32 has no
354
440
  // authoritative source.sessionID/callID on ShellCreateBefore.
355
- const cap = Number(options.bashCap) || 0
356
- if (cap > 0 && event.timeout > cap) event.timeout = cap
441
+ const capMilliseconds = (Number(options.bashCap) || 0) * 1000
442
+ if (capMilliseconds > 0 && (event.timeout === 0 || event.timeout > capMilliseconds)) event.timeout = capMilliseconds
357
443
  if (/\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(event.command)) throw Error("Broad process termination is prohibited; terminate only a captured child PID.")
358
444
  }))
359
445
 
360
- for (const kind of ["context", "compaction"] as const) registrations.push(await ctx.session.hook(kind, async event => {
361
- const { cwd, data } = await scope(event.sessionID)
362
- if (!sessionOptions(await ctx.session.get({ sessionID: event.sessionID })).delivery) return
363
- // Prevent bypass through built-in shell/edit, plugin and child-session tools,
364
- // and Code Mode while the participating-tool policy is locked. Runtime hooks
365
- // still repeat the check at invocation time; hiding a model tool is not the
366
- // authorization boundary by itself.
367
- const locked = !data.state || planningStatus(data.state, fingerprint(cwd, ["."])).locked
368
- if (locked && event.tools && typeof event.tools === "object") {
369
- for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name)) delete event.tools[name]
446
+ registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
447
+ const { cwd, data, active } = await scope(event.sessionID)
448
+ if (!active) return
449
+ const prompt = originalTask(event.prompt.text)
450
+ const routed = routeGuidance(prompt, { cwd, profiles })
451
+ const prior = data.state && !["verified", "blocked"].includes(data.state.status) ? data.guidanceProfiles || data.state.guidanceProfiles || [] : []
452
+ data.guidanceProfiles = [...new Set([...prior, ...routed.map(profile => profile.id)])]
453
+ data.lastPrompt = prompt
454
+ if (!data.state || ["verified", "blocked"].includes(data.state.status)) data.discovery = newDiscovery(!looksInformational(prompt))
455
+ data.touched = false
456
+ data.statusPoll = undefined
457
+ await ctx.storage.set(sessionKey(event.sessionID), data)
458
+ })))
459
+ registrations.push(await ctx.session.hook("context", async event => serialized(event.sessionID, async () => {
460
+ const { session, cwd, data, options: opts, active } = await scope(event.sessionID)
461
+ const reason = budgetReason(data.budget)
462
+ if (reason) {
463
+ data.budget.stopped ||= reason
464
+ await ctx.storage.set(sessionKey(event.sessionID), data)
465
+ await ctx.session.interrupt({ sessionID: event.sessionID, resume: false })
466
+ throw Error(`Delivery budget exhausted: ${reason}. Do not dispatch another model request until an explicit delivery-budget-reset.`)
370
467
  }
371
- if (!data.state || ["verified", "blocked"].includes(data.state.status)) {
372
- const checkpoint = discoveryCheckpoint(data.discovery)
373
- if (checkpoint) event.system.push({ type: "text", text: checkpoint })
468
+ if (!active || !isShipAgent(event.agent)) {
469
+ if (event.tools) for (const name of Object.keys(event.tools)) if (localToolName(name).startsWith("delivery_")) delete event.tools[name]
374
470
  return
375
471
  }
376
- const dir = runDirectory(cwd, event.sessionID, data.state.runId)
377
- const recovery = deliveryRecovery(data.state, { report: join(dir, "report.json"), planningStatus: planningStatus(data.state, fingerprint(cwd, ["."])) })
378
- const checkpoint = recoveryMessages([], recovery).map((message: { content: string }) => message.content).join("\n")
379
- event.system.push({ type: "text", text: `${data.guidance || DELIVERY_INVARIANTS}\n\nCurrent delivery recovery record (not authorization; recompute freshness before checks/finish):\n${checkpoint}\n\n${deliveryPhaseGuidance(data.state)}` })
380
- }))
381
-
382
- registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
383
- const { cwd, data, options: opts } = await scope(event.sessionID)
384
- if (!opts.delivery) return
385
- const workflow = data.state && !["verified", "blocked"].includes(data.state.status) ? data.state.workflowMode : opts.workflow
386
- const routed = routeGuidance(event.prompt.text, { cwd, profiles })
387
- let guidance = DELIVERY_INVARIANTS + "\n" + deliveryPhaseGuidance(data.state) + "\n" + policyDescription(workflow)
388
- if (workflow === "standard") guidance += "\nA valid required plan allows implementation; final evidence and review stay mandatory."
389
- if (/\b(test|typescript|route|backend|api)\b/i.test(event.prompt.text)) guidance += `\n${TEST_INFRASTRUCTURE_GUIDANCE}`
390
- if (data.discovery.active && workflow === "strict") guidance += `\n${PLANNING_START}`
391
- const routedText = formatGuidance(routed); if (routedText) guidance += `\n${routedText}`
392
- if (data.extraGuidance) guidance += `\nTask-specific delivery context:\n${data.extraGuidance}`
393
- if (data.required.length) guidance += `\nUser-owned required validators are enforced: ${JSON.stringify(data.required)}. Run every required check; do not bypass.`
394
- if (looksInformational(event.prompt.text)) guidance += `\nReceipt check: informational/read-only ask; answer directly or use verification none/advisory. Do not enter a repair loop.`
395
- data.lastPrompt = event.prompt.text
396
- data.guidance = `${guidance}\n\n${data.state ? deliveryRecovery(data.state, { report: join(runDirectory(cwd, event.sessionID, data.state.runId), "report.json") }) ? "Existing delivery state remains authoritative; do not trust summary claims over current report/fingerprint." : "" : ""}`
397
- const internal = event.metadata?.carthagentRecovery === true || event.metadata?.carthagentBudget === true
398
- if (!internal) { data.discovery = newDiscovery(!looksInformational(event.prompt.text)); data.nudges = 0; data.touched = false }
472
+ data.model = { providerID: event.model.providerID, id: event.model.id }
473
+ let imageInput: boolean | undefined
474
+ if (ctx.model) {
475
+ const models = await ctx.model.list({ location: { directory: session.location.directory } })
476
+ imageInput = models.data.find(model => model.providerID === event.model.providerID && model.id === event.model.id)?.capabilities.input.includes("image")
477
+ }
478
+ const activeProfiles = profiles.filter(profile => (data.guidanceProfiles || data.state?.guidanceProfiles || []).includes(profile.id))
479
+ event.system.push({ type: "text", text: shipGuidance({ state: data.state, workflow: String(opts.workflow || "standard"), prompt: data.lastPrompt, profiles: activeProfiles, discovery: data.discovery, extraGuidance: data.extraGuidance, required: data.required, imageInput }) })
480
+ if (data.budget.limits.tools || data.budget.limits.seconds || data.budget.limits.repairs) event.system.push({ type: "text", text: `Delivery budget: ${JSON.stringify(budgetSnapshot(data.budget))}. Reserve tools for verification/review/handoff; stop at exhaustion. Only the explicit delivery-budget-reset command authorizes a new budget.` })
481
+ const checkpoint = discoveryCheckpoint(data.discovery)
482
+ if (checkpoint) event.system.push({ type: "text", text: checkpoint })
483
+ if (event.tools && !data.state) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
484
+ if (data.state && !["verified", "blocked"].includes(data.state.status)) {
485
+ const dir = runDirectory(cwd, event.sessionID, data.state.runId)
486
+ const archived = archiveDeliveryExchanges(event.messages, { cwd, directory: dir, allow: Boolean(data.state.planning?.approval) })
487
+ event.messages = archived.messages
488
+ const summary = deliveryRecovery(data.state, { report: join(dir, "report.json") })
489
+ data.recovery ||= {}
490
+ const records = recoveryMessages([], summary, data.recovery)
491
+ const pressure = (data.contextBytes || 0) > 240000 ? "Context pressure: inspect the current recovery record and targeted source slices; avoid repeating whole captures." : ""
492
+ event.system.push({ type: "text", text: `${records.map((record: { content: string }) => record.content).join("\n")}\nInspect freshness before execution/finish; readyToFinish means finish, not polling.\n${pressure}\n${archived.failed ? "Archive unavailable: affected originals retained." : ""}\n${deliveryPhaseGuidance(data.state)}` })
493
+ try {
494
+ const metrics = contextMetrics({ system: event.system, messages: event.messages })
495
+ data.contextBytes = metrics.totalBytes
496
+ appendFileSync(join(dir, "context-metrics.jsonl"), JSON.stringify(metrics) + "\n")
497
+ } catch { /* Metrics are advisory, never an execution gate. */ }
498
+ if (!data.state.planning?.approval && data.state.workflowMode !== "standard" && event.tools) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
499
+ }
399
500
  await ctx.storage.set(sessionKey(event.sessionID), data)
400
501
  })))
401
-
502
+ registrations.push(await ctx.session.hook("compaction", async event => {
503
+ const { data, active } = await scope(event.sessionID)
504
+ if (!active || !isShipAgent(event.agent) || !data.state || ["verified", "blocked"].includes(data.state.status)) return
505
+ event.system.push({ type: "text", text: JSON.stringify(deliveryRecovery(data.state, { report: join(runDirectory(data.cwd!, event.sessionID, data.state.runId), "report.json") })) })
506
+ }))
402
507
  registrations.push(await ctx.command.transform(editor => {
403
508
  editor.add({ name: "delivery-budget-status", description: "Show this session's persistent delivery budget", execute: async ({ sessionID }) => {
404
509
  const { data } = await scope(sessionID)
405
- await ctx.session.prompt({ sessionID, delivery: "steer", metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
510
+ await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
406
511
  } })
407
512
  editor.add({ name: "delivery-budget-reset", description: "Explicitly authorize a fresh budget for this session", execute: async ({ sessionID }) => {
408
513
  const snapshot = await serialized(sessionID, async () => {
@@ -410,7 +515,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
410
515
  for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
411
516
  return budgetSnapshot(data.budget)
412
517
  })
413
- await ctx.session.prompt({ sessionID, delivery: "steer", metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
518
+ await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
414
519
  } })
415
520
  }))
416
521
 
@@ -422,7 +527,8 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
422
527
  const sessionID = event.data.sessionID
423
528
  let followup: string | undefined
424
529
  await serialized(sessionID, async () => {
425
- const { cwd, data, session } = await scope(sessionID)
530
+ const { cwd, data, session, active } = await scope(sessionID)
531
+ if (!active) return
426
532
  const reason = budgetReason(data.budget)
427
533
  if (reason && !data.budget.stopped) {
428
534
  data.budget.stopped = reason
@@ -432,7 +538,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
432
538
  }
433
539
  if (data.discovery.active) data.discovery = discoveryEvent(data.discovery, "blocked")
434
540
  const state = data.state
435
- if (!state || data.nudges >= 2 || ["verified", "blocked"].includes(state.status) || !data.touched || verificationMode(state.plan) !== "required" || session.outcome !== "succeeded") {
541
+ if (!state || !shouldContinue({ state, touched: data.touched, nudges: data.nudges, stopReason: session.outcome === "succeeded" ? "stop" : "error", pendingMessages: false, fresh: false })) {
436
542
  await ctx.storage.set(sessionKey(sessionID), data)
437
543
  return
438
544
  }
@@ -459,7 +565,11 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
459
565
  data.nudges++
460
566
  const pending = issues.pendingChecks.join(", ") || "none"
461
567
  await ctx.storage.set(sessionKey(sessionID), data)
462
- followup = `Delivery follow-up ${data.nudges}/2: the required contract has no fresh verified handoff. Pending checks: ${pending}. Inspect current delivery_status, repair the actual cause, rerun checks and finish verified, or record status=blocked with specific limitations. Do not repeat a success claim or weaken obligations.`
568
+ const failures = Object.entries(data.state.evidence || {}).filter(([, e]) => !(e as { passed: boolean }).passed).map(([id, e]) => {
569
+ const evidence = e as { code: number; outputTail?: string }
570
+ return `${id}: exit ${evidence.code}\n${evidence.outputTail || ""}`
571
+ }).join("\n")
572
+ followup = `${failures}\nDelivery follow-up ${data.nudges}/2: the required contract has no fresh verified handoff. Pending checks: ${pending}. Inspect current delivery_status, repair the actual cause, rerun checks and finish verified, or record status=blocked with specific limitations. Do not repeat a success claim or weaken obligations.`
463
573
  } catch (error) {
464
574
  console.error("Carthagent delivery recovery failed closed", error)
465
575
  } finally { release() }
@@ -484,5 +594,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
484
594
  function requiresImplementationTool(name: string) {
485
595
  // V2 exposes namespaced tools as `namespace_name` in model context and hooks.
486
596
  const toolName = name.startsWith("carthagent_") ? name.slice("carthagent_".length) : name
487
- return !["read", "ls", "find", "grep", "execute", "code_nav", "delivery_probe", "delivery_plan", "delivery_revise", "delivery_design", "delivery_status", "delivery_finish", "delivery-budget-status", "delivery-budget-reset"].includes(toolName)
597
+ return toolName !== "execute" && !["delivery-budget-status", "delivery-budget-reset"].includes(toolName) && requiresPlanApproval(toolName)
598
+ }
599
+
600
+ function planSnapshot(state: SessionState["state"]) {
601
+ const items = planSteps(state.plan).map((step: { id: string; title: string }, i: number) => ({ id: step.id, content: step.title, status: state.stepStatus[`step${i}`] || "pending" }))
602
+ return { runId: state.runId, revision: state.revision, status: state.status, items }
488
603
  }