carthagent 1.0.7 → 1.1.0-preview.12

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 (275) hide show
  1. package/README.md +80 -79
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +314 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  57. package/docs/release-notes-native-draft.md +73 -0
  58. package/docs/release-notes-preview.10.md +121 -0
  59. package/docs/release-notes-preview.11.md +76 -0
  60. package/docs/release-notes-preview.12.md +55 -0
  61. package/docs/release-notes-preview.9.md +130 -0
  62. package/evaluate.mjs +28 -1
  63. package/lib/budget.mjs +1 -1
  64. package/lib/codegraph.mjs +1 -1
  65. package/lib/delivery.mjs +3 -2
  66. package/lib/environment.mjs +8 -0
  67. package/lib/guidance.mjs +1 -1
  68. package/lib/opencode/cli-runtime.mjs +1171 -0
  69. package/lib/opencode/cli.mjs +252 -0
  70. package/lib/opencode/credentials.mjs +63 -8
  71. package/lib/opencode/demo.mjs +53 -0
  72. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  73. package/lib/opencode/evaluation.mjs +32 -12
  74. package/lib/opencode/headless.mjs +8 -4
  75. package/lib/opencode/provider-policy.mjs +23 -0
  76. package/lib/opencode/review.mjs +37 -16
  77. package/lib/opencode/runtime-artifact.mjs +335 -0
  78. package/lib/opencode/runtime-github-transport.mjs +121 -0
  79. package/lib/opencode/runtime-release.json +36 -0
  80. package/lib/opencode/runtime.mjs +298 -20
  81. package/lib/opencode/service.mjs +77 -35
  82. package/lib/opencode/session-options.mjs +6 -2
  83. package/lib/opencode/update.mjs +222 -0
  84. package/lib/opencode/workspace.mjs +1 -1
  85. package/lib/planning-advice.mjs +2 -2
  86. package/lib/tui/framing.mjs +1 -80
  87. package/lib/update.mjs +10 -1
  88. package/package.json +33 -7
  89. package/plugins/carthagent/README.md +23 -0
  90. package/plugins/carthagent/src/bootstrap.ts +1 -1
  91. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  92. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  93. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  94. package/plugins/carthagent/src/index.ts +71 -24
  95. package/plugins/carthagent/src/rpc.ts +51 -6
  96. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  97. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  98. package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
  99. package/plugins/carthagent/src/server/delivery.ts +217 -106
  100. package/plugins/carthagent/src/server/framing.mjs +80 -0
  101. package/plugins/carthagent/src/server/policy.ts +1 -1
  102. package/plugins/carthagent/src/server/providers.ts +58 -30
  103. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  104. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  105. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  106. package/plugins/carthagent/src/server/ship-guidance.ts +39 -0
  107. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  108. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  109. package/plugins/carthagent/src/testing.ts +5 -0
  110. package/plugins/carthagent/src/tui.tsx +492 -0
  111. package/plugins/carthagent/src/ui/client.ts +125 -0
  112. package/plugins/carthagent/src/ui/commands.ts +70 -0
  113. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  114. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  115. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  116. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  117. package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
  118. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  119. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  120. package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
  121. package/plugins/carthagent/src/ui/native-review.mjs +154 -0
  122. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  123. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  124. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  125. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  126. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  127. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  128. package/plugins/carthagent/src/ui/voyages-navigation.tsx +122 -0
  129. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  130. package/plugins/carthagent/src/ui/voyages.tsx +127 -0
  131. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  132. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  133. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  134. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  135. package/plugins/carthagent/tests/commands.test.ts +107 -0
  136. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  137. package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
  138. package/plugins/carthagent/themes/carthage.json +67 -0
  139. package/plugins/carthagent/tui.ts +26 -1
  140. package/script/ctg/build-owned-runtime.sh +166 -0
  141. package/tests/budget-engine.test.mjs +4 -3
  142. package/tests/cli.test.mjs +52 -96
  143. package/tests/daily-evidence.test.mjs +62 -0
  144. package/tests/delivery.test.mjs +22 -1
  145. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  146. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  147. package/tests/installed-runtime-contract.test.mjs +43 -0
  148. package/tests/native-cli-host-smoke.mjs +457 -0
  149. package/tests/native-daily-editor-focus.test.mjs +213 -0
  150. package/tests/native-daily-editor.test.mjs +43 -0
  151. package/tests/native-daily-quests-layout.test.mjs +53 -0
  152. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  153. package/tests/native-daily-quests-ui.test.mjs +778 -0
  154. package/tests/native-daily-source.test.mjs +426 -0
  155. package/tests/native-demo-ui.test.mjs +48 -0
  156. package/tests/native-evaluation-admission.test.mjs +24 -0
  157. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  158. package/tests/native-fixture-runtime.test.mjs +111 -0
  159. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  160. package/tests/native-glyph-pty.test.mjs +107 -0
  161. package/tests/native-manual-projects.test.mjs +170 -0
  162. package/tests/native-owned-runtime-env.test.mjs +140 -0
  163. package/tests/native-paired-guards.test.mjs +464 -0
  164. package/tests/native-provider-policy.test.mjs +31 -0
  165. package/tests/native-pty-invocation.test.mjs +114 -0
  166. package/tests/native-pty-runtime.test.mjs +45 -0
  167. package/tests/native-resume-session.test.mjs +46 -0
  168. package/tests/native-review-consumer.test.mjs +267 -0
  169. package/tests/native-runtime-identity.test.mjs +214 -0
  170. package/tests/native-ship-parity.test.mjs +526 -0
  171. package/tests/native-ship-plan.test.mjs +75 -0
  172. package/tests/native-ship-rpc.test.mjs +71 -0
  173. package/tests/native-ui-commands.test.mjs +6 -0
  174. package/tests/native-voyages-layout.test.mjs +28 -0
  175. package/tests/native-voyages-navigation.test.mjs +169 -0
  176. package/tests/native-voyages-rpc.test.mjs +395 -0
  177. package/tests/native-voyages-ui.test.mjs +277 -0
  178. package/tests/opencode-atomic-write.test.mjs +55 -0
  179. package/tests/opencode-cli.test.mjs +1312 -0
  180. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  181. package/tests/opencode-providers.test.mjs +245 -18
  182. package/tests/opencode-runtime.test.mjs +546 -8
  183. package/tests/opencode-session-controls.test.mjs +15 -0
  184. package/tests/opencode-ship-rpc.test.mjs +71 -0
  185. package/tests/opencode-update.test.mjs +475 -0
  186. package/tests/opencode-workspace.test.mjs +51 -2
  187. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  188. package/tests/release-workflow.test.mjs +46 -0
  189. package/tests/runtime-artifact.test.mjs +269 -0
  190. package/tests/runtime-development-trust.test.mjs +230 -0
  191. package/tests/runtime-github-transport.test.mjs +132 -0
  192. package/tests/runtime-token-env.test.mjs +23 -0
  193. package/tests/test_native_terminal_probe.py +76 -0
  194. package/tests/test_native_terminal_semantics.py +232 -0
  195. package/tests/update.test.mjs +36 -0
  196. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  197. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  198. package/tests/workspace-live.test.mjs +127 -0
  199. package/tools/benchmark/diagnose-request.mjs +59 -36
  200. package/tools/benchmark/eval-common.mjs +3 -3
  201. package/tools/benchmark/evaluators.test.mjs +4 -4
  202. package/tools/benchmark/guard.test.mjs +35 -2
  203. package/tools/benchmark/guard.ts +20 -7
  204. package/tools/benchmark/planning-startup.test.mjs +30 -34
  205. package/tools/benchmark/runner.mjs +12 -7
  206. package/tools/benchmark/runner.test.mjs +23 -7
  207. package/tools/benchmark/upstream-error.test.mjs +16 -2
  208. package/tools/generate-owned-runtime-release.mjs +217 -0
  209. package/tools/installed-runtime-contract.mjs +16 -0
  210. package/tools/manual-runtime-manifest.mjs +39 -0
  211. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  212. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  213. package/tools/native-fixture.mjs +199 -40
  214. package/tools/native-glyph-helpers.py +127 -0
  215. package/tools/native-glyph-pty.py +144 -0
  216. package/tools/native-manual-projects.mjs +259 -0
  217. package/tools/native-manual.mjs +338 -0
  218. package/tools/native-model-picker-acceptance.py +306 -0
  219. package/tools/native-owned-runtime-env.sh +47 -0
  220. package/tools/native-paired-guards.mjs +282 -0
  221. package/tools/native-paired-manifest.mjs +72 -0
  222. package/tools/native-paired-orchestration.sh +51 -0
  223. package/tools/native-plugin-readiness.mjs +33 -0
  224. package/tools/native-plugin-readiness.test.mjs +53 -0
  225. package/tools/native-pty-invocation.mjs +26 -0
  226. package/tools/native-pty-runtime.mjs +83 -0
  227. package/tools/native-pty-smoke.py +147 -24
  228. package/tools/native-public-repo-shell-result.mjs +13 -0
  229. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  230. package/tools/native-runtime-identity.mjs +201 -0
  231. package/tools/native-slash-acceptance-env.py +38 -0
  232. package/tools/native-terminal-semantics.py +876 -0
  233. package/tools/owned-shell-observer/index.ts +16 -0
  234. package/tools/owned-shell-observer/package.json +1 -0
  235. package/tools/owned-visual-clear-env.py +48 -0
  236. package/tools/owned-visual-clear-model.py +21 -0
  237. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  238. package/tools/prepare-native-manual.mjs +122 -0
  239. package/tools/run-native-terminal-paired.sh +403 -0
  240. package/tools/runtime-token-env.mjs +8 -0
  241. package/tools/seed-native-switch-fixture.mjs +79 -0
  242. package/tools/test-native-controls.mjs +3 -1
  243. package/tools/test-native-daily-quests.mjs +65 -0
  244. package/tools/test-native-default-cli.mjs +106 -0
  245. package/tools/test-native-delivery-host.mjs +100 -18
  246. package/tools/test-native-evaluation.mjs +7 -3
  247. package/tools/test-native-glyph-pty.mjs +94 -0
  248. package/tools/test-native-host.mjs +83 -61
  249. package/tools/test-native-outcomes.mjs +3 -1
  250. package/tools/test-native-package.mjs +206 -38
  251. package/tools/test-native-performance.mjs +26 -0
  252. package/tools/test-native-product.mjs +90 -0
  253. package/tools/test-native-pty.mjs +34 -9
  254. package/tools/test-native-public-repo.mjs +224 -0
  255. package/tools/test-native-refresh-reentry.mjs +106 -0
  256. package/tools/test-native-report.mjs +27 -20
  257. package/tools/test-native-review.mjs +75 -0
  258. package/tools/test-native-runtime.mjs +120 -0
  259. package/tools/test-native-ship-models.mjs +176 -0
  260. package/tools/test-native-ship-walkthrough.mjs +96 -0
  261. package/tools/test-native-slash-commands.mjs +125 -0
  262. package/tools/test-native-slash-commands.py +242 -0
  263. package/tools/test-native-swe-host.mjs +224 -55
  264. package/tools/test-native-tasks.mjs +77 -0
  265. package/tools/test-native-terminal-performance.py +887 -0
  266. package/tools/test-native-terminal-readiness.sh +85 -0
  267. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  268. package/tools/test-native-visual-clear-installed.mjs +48 -0
  269. package/tools/test-owned-host-controls.mjs +238 -0
  270. package/tools/test-owned-opencode-runtime.mjs +35 -0
  271. package/tools/test-owned-visual-clear-env.py +120 -0
  272. package/tools/test-owned-visual-clear.py +373 -0
  273. package/tools/verify-draft-runtime-gate.mjs +138 -0
  274. package/tools/verify-owned-runtime-release.mjs +114 -0
  275. 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>
@@ -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) {
@@ -141,9 +152,15 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
141
152
  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
153
  name, description, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
143
154
  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)
155
+ const { active } = await scope(tool.sessionID)
156
+ if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
157
+ try {
158
+ const result = await execute(value, tool)
159
+ const { data } = await scope(tool.sessionID)
160
+ if (!data.state) return result
161
+ return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
162
+ }
163
+ catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
147
164
  }, options: { namespace: "carthagent", codemode: true },
148
165
  })
149
166
 
@@ -154,7 +171,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
154
171
  const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
155
172
  validateRevision(data.state, plan)
156
173
  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: {} }
174
+ 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
175
  data.discovery = newDiscovery(); const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
159
176
  mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
160
177
  await persist(cwd, tool.sessionID, data)
@@ -172,23 +189,51 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
172
189
  await persist(cwd, tool.sessionID, data)
173
190
  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
191
  }))
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) => {
192
+ 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
193
  if (input.action === "guide") return text(planningGuide())
177
- return operate(tool.sessionID, async ({ cwd, data }) => {
194
+ return operate(tool.sessionID, async ({ session, cwd, data, options: opts }) => {
178
195
  if (!data.state) throw Error("Call delivery_plan first")
179
196
  const hash = fingerprint(cwd, ["."]); let result
180
197
  if (input.action === "validate") result = validatePlanning(data.state, hash)
181
198
  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 })
199
+ else if (input.action === "review") result = recordPlanReview(data.state, hash, normalizeDesignReview(input))
183
200
  else if (input.action === "approve") result = approvePlanning(data.state, hash)
184
201
  else throw Error("Unknown delivery_design action")
202
+ let advice
203
+ if (input.action === "inspect") {
204
+ const ship = (session.model ?? data.model)?.providerID === "experiential-labs"
205
+ const enabled = opts.jev === undefined ? ship : opts.jev === true
206
+ const createAdvisor = () => {
207
+ if (opts.jevProvider === "ship" || (ship && !opts.jevProvider)) {
208
+ return { classify: async (plan: unknown) => {
209
+ const active = await ctx.integration.connection.active("experiential-labs")
210
+ if (!active || active.type !== "credential" || active.method !== "oauth") throw Error("Ship OAuth connection required")
211
+ const credential = await ctx.integration.connection.resolve(active)
212
+ if (credential?.type !== "oauth" || credential.methodID !== "carthagent-device" || !credential.access) throw Error("Ship OAuth credential required")
213
+ return createShipJev({ fetchImpl: globalThis.fetch, ...{ accessToken: credential.access, baseUrl: cloudGatewayUrl(), signal: tool.signal } }).classify(plan)
214
+ } }
215
+ }
216
+ return configuredAdvisor({ ...{ enabled, provider: String(opts.jevProvider || "openrouter"), signal: tool.signal } })
217
+ }
218
+ advice = await inspectAdvice(data.state, result, { ...{ enabled, createAdvisor, persist: () => persist(cwd, tool.sessionID, data) } })
219
+ }
220
+ let receipt = designReceipt(input.action, result)
221
+ if (input.action === "inspect" && JSON.stringify(result).length > 16000) {
222
+ try {
223
+ const archive = archiveContext(cwd, runDirectory(cwd, tool.sessionID, data.state.runId), result)
224
+ 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." }
225
+ } catch { /* Retain the original capture if archival fails. */ }
226
+ }
185
227
  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) })
228
+ 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
229
  })
188
230
  })
189
231
  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
232
  if (!data.state) return text("No delivery contract.")
191
- const dir = runDirectory(cwd, tool.sessionID, data.state.runId); const hash = fingerprint(cwd, ["."])
233
+ const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
234
+ let hash: string
235
+ try { hash = fingerprint(cwd, ["."]) }
236
+ 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
237
  const issues = completionIssues(data.state, cwd, hash); const readyToFinish = Object.values(issues).every(items => !items.length)
193
238
  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
239
  }))
@@ -212,10 +257,25 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
212
257
  for (const check of selected) {
213
258
  if (tool.signal.aborted) throw Error("Check cancelled before execution")
214
259
  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)
260
+ let before: string
261
+ try { before = fingerprint(cwd, ["."]) }
262
+ catch (error) {
263
+ data.state.evidence[check.id] = { passed: false, ...identity, fingerprint: null, code: 1, outputTail: `Evidence scope error: ${error}` }
264
+ await persist(cwd, tool.sessionID, data)
265
+ throw Error(`Evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations rather than retrying.`)
266
+ }
267
+ await persist(cwd, tool.sessionID, data)
268
+ await tool.progress?.({ title: `Running ${check.id}`, description: JSON.stringify(check.argv) })
216
269
  const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
217
270
  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
271
+ let after: string
272
+ try { after = fingerprint(cwd, ["."]) }
273
+ catch (error) {
274
+ 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)}` }
275
+ await persist(cwd, tool.sessionID, data)
276
+ throw Error(`Post-check evidence scope error: ${error}. Call delivery_finish with status="blocked" and specific limitations.`)
277
+ }
278
+ const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
219
279
  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) }
220
280
  results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
221
281
  await persist(cwd, tool.sessionID, data)
@@ -223,7 +283,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
223
283
  const failures = results.filter(r => !r.passed && verification === "required")
224
284
  if (failures.length) throw Error(JSON.stringify({ failures, next: "Repair actual failures; do not weaken obligations." }))
225
285
  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) })
286
+ 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
287
  }))
228
288
  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 }) => {
229
289
  if (!data.state) throw Error("Call delivery_plan first")
@@ -235,17 +295,20 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
235
295
  if (data.configError) throw Error(data.configError)
236
296
  const verification = verificationMode(data.state.plan); let hash: string | null = null
237
297
  if (input.status === "verified") {
238
- hash = fingerprint(cwd, ["."])
239
298
  if (verification === "required") {
299
+ hash = fingerprint(cwd, ["."])
240
300
  const issues = completionIssues(data.state, cwd, hash)
241
301
  if (Object.values(issues).some(items => items.length)) throw Error(`Cannot verify: ${JSON.stringify(issues)}`)
242
- }
302
+ } else { try { hash = fingerprint(cwd, ["."]) } catch { hash = null } }
243
303
  } else {
244
304
  if (!input.limitations.length) throw Error("Blocked delivery requires an explicit limitation/reason")
245
305
  try { hash = fingerprint(cwd, ["."]) } catch { hash = "unfingerprinted:scope_exceeded" }
246
306
  }
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") })
307
+ const advisory = verification !== "required"
308
+ const advisoryFailures = Object.entries(data.state.evidence).filter(([, value]) => !(value as { passed: boolean }).passed).map(([id]) => id)
309
+ 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) }
310
+ await persist(cwd, tool.sessionID, data)
311
+ 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
312
  }))
250
313
  }))
251
314
 
@@ -256,35 +319,50 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
256
319
  // share the outer call ID/message ID while still being distinct invocations.
257
320
  const callKey = `${event.sessionID}\0${event.messageID}\0${event.id}\0${event.tool}`
258
321
  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
322
+ const { cwd, data, options: opts, active: selected } = await scope(event.sessionID)
323
+ const active = selected && isShipAgent(event.agent)
324
+ const toolName = localToolName(event.tool)
325
+ if (toolName.startsWith("delivery_") && !active) throw Error("Delivery tools require Ship mode.")
326
+ if (!active && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
261
327
  let persistenceAttempted = false
262
328
  try {
263
329
  const nativeShell = ["bash", "shell", "powershell"].includes(event.tool)
264
330
  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}).`)
331
+ if (active && toolName === "delivery_status" && data.state) {
332
+ let source
333
+ try { source = fingerprint(cwd, ["."]) } catch (error) { source = String(error) }
334
+ 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])
335
+ data.statusPoll = { key, count: data.statusPoll?.key === key ? data.statusPoll.count + 1 : 1 }
336
+ if (data.statusPoll.count > 3) throw Error("Repeated unchanged delivery_status polling is blocked. Execute the next action or finish instead.")
337
+ } else if (active && (toolName.startsWith("delivery_") || ["write", "edit", "patch"].includes(toolName))) data.statusPoll = undefined
338
+ if (active && event.tool !== "execute" && requiresImplementationTool(toolName)) {
339
+ if (!data.state) {
340
+ data.discovery = discoveryEvent(data.discovery, "blocked")
341
+ throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}). ${discoveryCheckpoint(data.discovery) || ""}`)
342
+ }
267
343
  const release = lockWorkspace(cwd)
268
344
  try {
269
345
  const dir = runDirectory(cwd, event.sessionID, data.state.runId)
270
346
  assertActiveReport(cwd, join(dir, "report.json"))
271
347
  data.state = reconcileReport(join(dir, "report.json"), data.state)
272
- enforceToolPolicy(event.tool, data.state, cwd)
348
+ enforceToolPolicy(toolName, data.state, cwd)
273
349
  await persist(cwd, event.sessionID, data)
274
350
  } finally { release() }
275
351
  }
276
- if ((event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
352
+ if (active && (event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
277
353
  const path = input.path.replaceAll("\\", "/"), cwdPath = cwd.replaceAll("\\", "/").replace(/^\/+/, ""), name = basename(cwd)
278
354
  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
355
  if (event.tool === "write" && data.initialFiles.includes(resolve(cwd, input.path))) throw Error(`Preserve existing ${input.path}; use focused edits.`)
280
356
  }
281
357
  const cap = Number(opts.bashCap) || 0
282
- if (nativeShell) {
358
+ if (active && nativeShell) {
283
359
  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
360
+ const protection = shellProtection(command, opts.protectExisting ? data.initialFiles : [])
361
+ if (protection) throw Error(protection)
362
+ const capMilliseconds = cap * 1000
363
+ if (cap > 0 && (typeof input.timeout !== "number" || input.timeout === 0 || input.timeout > capMilliseconds)) input.timeout = capMilliseconds
286
364
  }
287
- if (["write", "edit", "patch"].includes(event.tool) && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
365
+ if (active && event.tool === "write" && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
288
366
  data.writeCounts ||= {}
289
367
  const path = String(input.path || "")
290
368
  const count = data.writeCounts[path] || 0
@@ -317,92 +395,115 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
317
395
  })))
318
396
 
319
397
  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
- }
398
+ const { cwd, data, options: opts, active } = await scope(event.sessionID)
399
+ if (!active || !isShipAgent(event.agent)) return
400
+ const tool = localToolName(event.tool)
325
401
  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)) {
402
+ if (!failed && ["write", "edit", "patch"].includes(tool)) data.touched = true
403
+ if (data.discovery.active && ["read", "ls", "glob", "find", "grep", "code_nav"].includes(tool)) data.discovery = discoveryEvent(data.discovery, "read")
404
+ const rawContent = event.status === "error" ? event.error.message : event.result?.content
405
+ const content = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent || []
406
+ const diagnostic = probeDiagnostics(data.probeState as Parameters<typeof probeDiagnostics>[0], { toolName: tool === "shell" ? "bash" : tool, isError: failed, content })
407
+ data.probeState = diagnostic.state
408
+ const cap = Math.max(0, Number(opts.toolOutputCap) || 0)
409
+ if (cap && event.status === "completed" && ["bash", "shell", "read", "grep", "glob", "find", "ls", "code_nav"].includes(tool)) {
410
+ 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) }
411
+ }
412
+ if (diagnostic.message) {
413
+ 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 }] }
414
+ else event.error = new NativeToolError({ message: `${event.error.message}\n\n${diagnostic.message}` })
415
+ }
416
+ if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(tool)) {
329
417
  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)
418
+ const release = lockWorkspace(cwd)
419
+ try {
420
+ assertActiveReport(cwd, join(dir, "report.json"))
421
+ data.state = reconcileReport(join(dir, "report.json"), data.state)
422
+ writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
423
+ } finally { release() }
331
424
  }
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 }
425
+ if (event.status === "completed" && data.state && (tool === "execute" || tool.startsWith("delivery_"))) {
426
+ event.result = { ...event.result, metadata: { ...event.result.metadata, deliveryPlan: planSnapshot(data.state) } }
347
427
  }
348
428
  await ctx.storage.set(sessionKey(event.sessionID), data)
429
+ const delay = Math.min(30000, Math.max(0, Number(opts.turnDelayMs) || 0) * Math.max(1, Math.ceil((data.contextBytes || 0) / 4 / 50000)))
430
+ if (delay) await new Promise(resolve => setTimeout(resolve, delay))
349
431
  })))
350
432
 
351
433
  registrations.push(await ctx.shell.hook("create.before", async event => {
352
434
  // Session-scoped policy, limits, and budget were handled at execute.before.
353
435
  // This hook intentionally does not attribute commands: 2.0.32 has no
354
436
  // authoritative source.sessionID/callID on ShellCreateBefore.
355
- const cap = Number(options.bashCap) || 0
356
- if (cap > 0 && event.timeout > cap) event.timeout = cap
437
+ const capMilliseconds = (Number(options.bashCap) || 0) * 1000
438
+ if (capMilliseconds > 0 && (event.timeout === 0 || event.timeout > capMilliseconds)) event.timeout = capMilliseconds
357
439
  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
440
  }))
359
441
 
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]
442
+ registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
443
+ const { cwd, data, active } = await scope(event.sessionID)
444
+ if (!active) return
445
+ const prompt = originalTask(event.prompt.text)
446
+ const routed = routeGuidance(prompt, { cwd, profiles })
447
+ const prior = data.state && !["verified", "blocked"].includes(data.state.status) ? data.guidanceProfiles || data.state.guidanceProfiles || [] : []
448
+ data.guidanceProfiles = [...new Set([...prior, ...routed.map(profile => profile.id)])]
449
+ data.lastPrompt = prompt
450
+ if (!data.state || ["verified", "blocked"].includes(data.state.status)) data.discovery = newDiscovery(!looksInformational(prompt))
451
+ data.touched = false
452
+ data.statusPoll = undefined
453
+ await ctx.storage.set(sessionKey(event.sessionID), data)
454
+ })))
455
+ registrations.push(await ctx.session.hook("context", async event => serialized(event.sessionID, async () => {
456
+ const { session, cwd, data, options: opts, active } = await scope(event.sessionID)
457
+ const reason = budgetReason(data.budget)
458
+ if (reason) {
459
+ data.budget.stopped ||= reason
460
+ await ctx.storage.set(sessionKey(event.sessionID), data)
461
+ await ctx.session.interrupt({ sessionID: event.sessionID, resume: false })
462
+ throw Error(`Delivery budget exhausted: ${reason}. Do not dispatch another model request until an explicit delivery-budget-reset.`)
370
463
  }
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 })
464
+ if (!active || !isShipAgent(event.agent)) {
465
+ if (event.tools) for (const name of Object.keys(event.tools)) if (localToolName(name).startsWith("delivery_")) delete event.tools[name]
374
466
  return
375
467
  }
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 }
468
+ data.model = { providerID: event.model.providerID, id: event.model.id }
469
+ let imageInput: boolean | undefined
470
+ if (ctx.model) {
471
+ const models = await ctx.model.list({ location: { directory: session.location.directory } })
472
+ imageInput = models.data.find(model => model.providerID === event.model.providerID && model.id === event.model.id)?.capabilities.input.includes("image")
473
+ }
474
+ const activeProfiles = profiles.filter(profile => (data.guidanceProfiles || data.state?.guidanceProfiles || []).includes(profile.id))
475
+ 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 }) })
476
+ 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.` })
477
+ const checkpoint = discoveryCheckpoint(data.discovery)
478
+ if (checkpoint) event.system.push({ type: "text", text: checkpoint })
479
+ if (event.tools && !data.state) for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name) && name !== "execute") delete event.tools[name]
480
+ if (data.state && !["verified", "blocked"].includes(data.state.status)) {
481
+ const dir = runDirectory(cwd, event.sessionID, data.state.runId)
482
+ const archived = archiveDeliveryExchanges(event.messages, { cwd, directory: dir, allow: Boolean(data.state.planning?.approval) })
483
+ event.messages = archived.messages
484
+ const summary = deliveryRecovery(data.state, { report: join(dir, "report.json") })
485
+ data.recovery ||= {}
486
+ const records = recoveryMessages([], summary, data.recovery)
487
+ const pressure = (data.contextBytes || 0) > 240000 ? "Context pressure: inspect the current recovery record and targeted source slices; avoid repeating whole captures." : ""
488
+ 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)}` })
489
+ try {
490
+ const metrics = contextMetrics({ system: event.system, messages: event.messages })
491
+ data.contextBytes = metrics.totalBytes
492
+ appendFileSync(join(dir, "context-metrics.jsonl"), JSON.stringify(metrics) + "\n")
493
+ } catch { /* Metrics are advisory, never an execution gate. */ }
494
+ 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]
495
+ }
399
496
  await ctx.storage.set(sessionKey(event.sessionID), data)
400
497
  })))
401
-
498
+ registrations.push(await ctx.session.hook("compaction", async event => {
499
+ const { data, active } = await scope(event.sessionID)
500
+ if (!active || !isShipAgent(event.agent) || !data.state || ["verified", "blocked"].includes(data.state.status)) return
501
+ event.system.push({ type: "text", text: JSON.stringify(deliveryRecovery(data.state, { report: join(runDirectory(data.cwd!, event.sessionID, data.state.runId), "report.json") })) })
502
+ }))
402
503
  registrations.push(await ctx.command.transform(editor => {
403
504
  editor.add({ name: "delivery-budget-status", description: "Show this session's persistent delivery budget", execute: async ({ sessionID }) => {
404
505
  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))}` })
506
+ await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
406
507
  } })
407
508
  editor.add({ name: "delivery-budget-reset", description: "Explicitly authorize a fresh budget for this session", execute: async ({ sessionID }) => {
408
509
  const snapshot = await serialized(sessionID, async () => {
@@ -410,7 +511,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
410
511
  for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
411
512
  return budgetSnapshot(data.budget)
412
513
  })
413
- await ctx.session.prompt({ sessionID, delivery: "steer", metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
514
+ await ctx.session.synthetic({ sessionID, resume: false, metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
414
515
  } })
415
516
  }))
416
517
 
@@ -422,7 +523,8 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
422
523
  const sessionID = event.data.sessionID
423
524
  let followup: string | undefined
424
525
  await serialized(sessionID, async () => {
425
- const { cwd, data, session } = await scope(sessionID)
526
+ const { cwd, data, session, active } = await scope(sessionID)
527
+ if (!active) return
426
528
  const reason = budgetReason(data.budget)
427
529
  if (reason && !data.budget.stopped) {
428
530
  data.budget.stopped = reason
@@ -432,7 +534,7 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
432
534
  }
433
535
  if (data.discovery.active) data.discovery = discoveryEvent(data.discovery, "blocked")
434
536
  const state = data.state
435
- if (!state || data.nudges >= 2 || ["verified", "blocked"].includes(state.status) || !data.touched || verificationMode(state.plan) !== "required" || session.outcome !== "succeeded") {
537
+ if (!state || !shouldContinue({ state, touched: data.touched, nudges: data.nudges, stopReason: session.outcome === "succeeded" ? "stop" : "error", pendingMessages: false, fresh: false })) {
436
538
  await ctx.storage.set(sessionKey(sessionID), data)
437
539
  return
438
540
  }
@@ -459,7 +561,11 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
459
561
  data.nudges++
460
562
  const pending = issues.pendingChecks.join(", ") || "none"
461
563
  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.`
564
+ const failures = Object.entries(data.state.evidence || {}).filter(([, e]) => !(e as { passed: boolean }).passed).map(([id, e]) => {
565
+ const evidence = e as { code: number; outputTail?: string }
566
+ return `${id}: exit ${evidence.code}\n${evidence.outputTail || ""}`
567
+ }).join("\n")
568
+ 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
569
  } catch (error) {
464
570
  console.error("Carthagent delivery recovery failed closed", error)
465
571
  } finally { release() }
@@ -484,5 +590,10 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
484
590
  function requiresImplementationTool(name: string) {
485
591
  // V2 exposes namespaced tools as `namespace_name` in model context and hooks.
486
592
  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)
593
+ return toolName !== "execute" && !["delivery-budget-status", "delivery-budget-reset"].includes(toolName) && requiresPlanApproval(toolName)
594
+ }
595
+
596
+ function planSnapshot(state: SessionState["state"]) {
597
+ 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" }))
598
+ return { runId: state.runId, revision: state.revision, status: state.status, items }
488
599
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Behind-the-scenes phased-delivery framing (the carthagent default flow).
3
+ *
4
+ * Every typed task is a phased delivery run: the raw text is what the user
5
+ * sees echoed in the feed, while the engine receives the expanded delivery template
6
+ * body. Resolving the template here (rather than forwarding "/guide <task>"
7
+ * to the engine) keeps the behavior identical in --isolate and headless -p modes,
8
+ * and keeps the machinery invisible in the feed.
9
+ *
10
+ * This is the default flow for carthagent — the whole product is built around it —
11
+ * so the input pill stays empty and the hint rotates through generic
12
+ * builder one-liners.
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join, dirname, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const ROOT = resolve(__dirname, '../../../..');
20
+ const TEMPLATE_PATH = join(ROOT, 'prompts', 'delivery.md');
21
+
22
+ let cached = null;
23
+
24
+ export const FRAME_HINTS = ['Message carthagent…'];
25
+
26
+ export function frameHint() {
27
+ return FRAME_HINTS[0];
28
+ }
29
+
30
+ function readTemplate(path) {
31
+ let body = '';
32
+ try {
33
+ const raw = readFileSync(path, 'utf8');
34
+ const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
35
+ body = (fm ? raw.slice(fm[0].length) : raw).trim();
36
+ } catch { body = ''; }
37
+ return body;
38
+ }
39
+
40
+ /** Body of prompts/delivery.md with frontmatter stripped. '' when unreadable. */
41
+ export function framingTemplate(path = TEMPLATE_PATH) {
42
+ if (path === TEMPLATE_PATH) {
43
+ if (cached == null) cached = readTemplate(TEMPLATE_PATH);
44
+ return cached;
45
+ }
46
+ return readTemplate(path);
47
+ }
48
+
49
+ /** Expand the delivery template for a task. Falls back to the bare task text. */
50
+ export function framePrompt(task, template = framingTemplate()) {
51
+ const t = String(task).trim();
52
+ if (!template) return t;
53
+ return template.includes('$@') ? template.replaceAll('$@', t) : `${template}\n\n${t}`;
54
+ }
55
+
56
+ /** Recover the original task text from a framed prompt, else null. */
57
+ export function unframe(text) {
58
+ // Match the current template suffix first: the task itself may contain
59
+ // blank lines (or even phase headings). Keep the legacy format fallback.
60
+ const value = String(text);
61
+ const template = framingTemplate();
62
+ const parts = template.split('$@');
63
+ if (parts.length === 2 && value.startsWith(parts[0]) && value.endsWith(parts[1])) {
64
+ return value.slice(parts[0].length, parts[1] ? -parts[1].length : undefined).trim();
65
+ }
66
+ const m = value.match(/^Deliver this task in phases(?: with executable evidence)?:\s*([\s\S]*?)\n\n/);
67
+ return m ? m[1].trim() : null;
68
+ }
69
+
70
+ /** True when a submitted prompt should get the delivery framing. */
71
+ export function shouldFrame(text, { enabled = true, delivery = true } = {}) {
72
+ if (!enabled || !delivery) return false;
73
+ const v = String(text).trim();
74
+ if (!v) return false;
75
+ if (v.startsWith('/') || v.startsWith('!')) return false;
76
+ // Questions/read-only asks are not delivery runs.
77
+ if (v.endsWith('?') && v.length < 300) return false;
78
+ if (/^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) && v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v)) return false;
79
+ return existsSync(TEMPLATE_PATH);
80
+ }