carthagent 1.0.7 → 1.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) 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/cloud-onboarding-acceptance.md +65 -0
  6. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  7. package/docs/glyph-review/test-isolation-v1.md +26 -0
  8. package/docs/glyph-review/typecheck-base.log +1 -0
  9. package/docs/glyph-review/typecheck-v4.log +1 -0
  10. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  11. package/docs/help-dispatch-review.md +69 -0
  12. package/docs/launch-readiness-live-results.md +65 -0
  13. package/docs/legacy-cli.md +150 -0
  14. package/docs/live-theme-plugin-api.md +18 -0
  15. package/docs/native-cli.md +102 -0
  16. package/docs/native-compatibility.md +16 -0
  17. package/docs/native-daily-quests.md +132 -0
  18. package/docs/native-glyph-pty.md +59 -0
  19. package/docs/native-manual-testing.md +336 -0
  20. package/docs/native-slash-acceptance-handoff.md +49 -0
  21. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  22. package/docs/native-terminal-performance.md +310 -0
  23. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  24. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  25. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  34. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  35. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  36. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  37. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  38. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  39. package/docs/native-terminal-semantics.md +89 -0
  40. package/docs/native-upgrade.md +175 -0
  41. package/docs/opencode-composed-source-review.md +89 -0
  42. package/docs/opencode-migration.md +225 -9
  43. package/docs/opencode-owned-runtime.md +241 -0
  44. package/docs/opencode-runtime.md +12 -0
  45. package/docs/opencode-theme-provider-proof/README.md +55 -0
  46. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  47. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  48. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  49. package/docs/owned-host-controls-proof.md +39 -0
  50. package/docs/owned-runtime-release-manifest.md +42 -0
  51. package/docs/owned-visual-clear-proof.md +40 -0
  52. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  53. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  54. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  55. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  61. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  62. package/docs/release-notes-beta.1.md +60 -0
  63. package/docs/release-notes-beta.2.md +60 -0
  64. package/docs/release-notes-native-draft.md +73 -0
  65. package/docs/release-notes-preview.10.md +121 -0
  66. package/docs/release-notes-preview.11.md +76 -0
  67. package/docs/release-notes-preview.12.md +55 -0
  68. package/docs/release-notes-preview.13.md +68 -0
  69. package/docs/release-notes-preview.9.md +130 -0
  70. package/docs/source-preview.md +120 -0
  71. package/evaluate.mjs +28 -1
  72. package/lib/budget.mjs +1 -1
  73. package/lib/cloud/client.mjs +13 -7
  74. package/lib/codegraph.mjs +1 -1
  75. package/lib/delivery-review.mjs +19 -6
  76. package/lib/delivery.mjs +3 -2
  77. package/lib/environment.mjs +8 -0
  78. package/lib/guidance.mjs +1 -1
  79. package/lib/opencode/cli-runtime.mjs +1188 -0
  80. package/lib/opencode/cli.mjs +253 -0
  81. package/lib/opencode/credentials.mjs +63 -8
  82. package/lib/opencode/demo.mjs +53 -0
  83. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  84. package/lib/opencode/evaluation.mjs +32 -12
  85. package/lib/opencode/headless.mjs +22 -5
  86. package/lib/opencode/provider-policy.mjs +23 -0
  87. package/lib/opencode/review.mjs +37 -16
  88. package/lib/opencode/runtime-artifact.mjs +335 -0
  89. package/lib/opencode/runtime-github-transport.mjs +121 -0
  90. package/lib/opencode/runtime-release.json +36 -0
  91. package/lib/opencode/runtime.mjs +299 -20
  92. package/lib/opencode/service.mjs +77 -35
  93. package/lib/opencode/session-options.mjs +6 -2
  94. package/lib/opencode/update.mjs +222 -0
  95. package/lib/opencode/workspace.mjs +1 -1
  96. package/lib/planning-advice.mjs +2 -2
  97. package/lib/providers/experiential.mjs +2 -2
  98. package/lib/providers/names.mjs +1 -1
  99. package/lib/tui/framing.mjs +1 -80
  100. package/lib/tui/models.mjs +1 -1
  101. package/lib/update.mjs +10 -1
  102. package/package.json +33 -7
  103. package/plugins/carthagent/README.md +23 -0
  104. package/plugins/carthagent/src/bootstrap.ts +1 -1
  105. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  106. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  107. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  108. package/plugins/carthagent/src/index.ts +71 -24
  109. package/plugins/carthagent/src/rpc.ts +51 -6
  110. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  111. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  112. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  113. package/plugins/carthagent/src/server/delivery.ts +227 -112
  114. package/plugins/carthagent/src/server/framing.mjs +80 -0
  115. package/plugins/carthagent/src/server/policy.ts +1 -1
  116. package/plugins/carthagent/src/server/providers.ts +65 -36
  117. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  118. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  119. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  120. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  121. package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
  122. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  123. package/plugins/carthagent/src/testing.ts +5 -0
  124. package/plugins/carthagent/src/tui.tsx +506 -0
  125. package/plugins/carthagent/src/ui/client.ts +127 -0
  126. package/plugins/carthagent/src/ui/commands.ts +70 -0
  127. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  128. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  129. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  130. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  131. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  132. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  133. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  134. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  135. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  136. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  137. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  138. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  139. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  140. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  141. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  142. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  143. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  144. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  145. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  146. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  147. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  148. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  149. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  150. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  151. package/plugins/carthagent/tests/commands.test.ts +107 -0
  152. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  153. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  154. package/plugins/carthagent/themes/carthage.json +67 -0
  155. package/plugins/carthagent/tui.ts +26 -1
  156. package/script/ctg/build-owned-runtime.sh +166 -0
  157. package/tests/budget-engine.test.mjs +4 -3
  158. package/tests/cli.test.mjs +52 -96
  159. package/tests/cloud-client.test.mjs +2 -2
  160. package/tests/daily-evidence.test.mjs +62 -0
  161. package/tests/delivery-review.test.mjs +57 -2
  162. package/tests/delivery.test.mjs +22 -1
  163. package/tests/experiential-extension.test.mjs +2 -2
  164. package/tests/experiential.test.mjs +1 -1
  165. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  166. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  167. package/tests/installed-runtime-contract.test.mjs +43 -0
  168. package/tests/native-cli-host-smoke.mjs +457 -0
  169. package/tests/native-daily-editor-focus.test.mjs +213 -0
  170. package/tests/native-daily-editor.test.mjs +43 -0
  171. package/tests/native-daily-quests-layout.test.mjs +55 -0
  172. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  173. package/tests/native-daily-quests-ui.test.mjs +778 -0
  174. package/tests/native-daily-source.test.mjs +525 -0
  175. package/tests/native-demo-ui.test.mjs +48 -0
  176. package/tests/native-evaluation-admission.test.mjs +24 -0
  177. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  178. package/tests/native-fixture-runtime.test.mjs +111 -0
  179. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  180. package/tests/native-glyph-pty.test.mjs +107 -0
  181. package/tests/native-manual-projects.test.mjs +170 -0
  182. package/tests/native-model-command.test.mjs +67 -0
  183. package/tests/native-owned-runtime-env.test.mjs +140 -0
  184. package/tests/native-paired-guards.test.mjs +464 -0
  185. package/tests/native-provider-policy.test.mjs +33 -0
  186. package/tests/native-pty-invocation.test.mjs +114 -0
  187. package/tests/native-pty-runtime.test.mjs +45 -0
  188. package/tests/native-resume-session.test.mjs +46 -0
  189. package/tests/native-review-consumer.test.mjs +286 -0
  190. package/tests/native-runtime-identity.test.mjs +214 -0
  191. package/tests/native-select-mouse.test.mjs +23 -0
  192. package/tests/native-ship-guidance.test.mjs +16 -0
  193. package/tests/native-ship-parity.test.mjs +526 -0
  194. package/tests/native-ship-plan.test.mjs +85 -0
  195. package/tests/native-ship-rpc.test.mjs +83 -0
  196. package/tests/native-ui-commands.test.mjs +6 -0
  197. package/tests/native-voyages-layout.test.mjs +28 -0
  198. package/tests/native-voyages-navigation.test.mjs +169 -0
  199. package/tests/native-voyages-rpc.test.mjs +397 -0
  200. package/tests/native-voyages-ui.test.mjs +307 -0
  201. package/tests/opencode-atomic-write.test.mjs +55 -0
  202. package/tests/opencode-cli.test.mjs +1312 -0
  203. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  204. package/tests/opencode-delivery.test.mjs +16 -0
  205. package/tests/opencode-providers.test.mjs +248 -20
  206. package/tests/opencode-runtime.test.mjs +597 -8
  207. package/tests/opencode-session-controls.test.mjs +15 -0
  208. package/tests/opencode-ship-rpc.test.mjs +71 -0
  209. package/tests/opencode-update.test.mjs +475 -0
  210. package/tests/opencode-workspace.test.mjs +51 -2
  211. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  212. package/tests/release-workflow.test.mjs +46 -0
  213. package/tests/runtime-artifact.test.mjs +269 -0
  214. package/tests/runtime-development-trust.test.mjs +231 -0
  215. package/tests/runtime-github-transport.test.mjs +132 -0
  216. package/tests/runtime-token-env.test.mjs +23 -0
  217. package/tests/server.test.mjs +3 -1
  218. package/tests/ship-telemetry.test.mjs +1 -1
  219. package/tests/source-preview.test.mjs +253 -0
  220. package/tests/test_native_terminal_probe.py +76 -0
  221. package/tests/test_native_terminal_semantics.py +232 -0
  222. package/tests/tui.test.mjs +2 -2
  223. package/tests/update.test.mjs +42 -5
  224. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  225. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  226. package/tests/workspace-live.test.mjs +127 -0
  227. package/tools/benchmark/diagnose-request.mjs +59 -36
  228. package/tools/benchmark/eval-common.mjs +3 -3
  229. package/tools/benchmark/evaluators.test.mjs +4 -4
  230. package/tools/benchmark/guard.test.mjs +35 -2
  231. package/tools/benchmark/guard.ts +20 -7
  232. package/tools/benchmark/planning-startup.test.mjs +30 -34
  233. package/tools/benchmark/runner.mjs +12 -7
  234. package/tools/benchmark/runner.test.mjs +23 -7
  235. package/tools/benchmark/upstream-error.test.mjs +16 -2
  236. package/tools/generate-owned-runtime-release.mjs +217 -0
  237. package/tools/installed-runtime-contract.mjs +16 -0
  238. package/tools/manual-runtime-manifest.mjs +39 -0
  239. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  240. package/tools/native-fixture-stop-diagnostics.mjs +45 -0
  241. package/tools/native-fixture.mjs +199 -40
  242. package/tools/native-glyph-helpers.py +127 -0
  243. package/tools/native-glyph-pty.py +144 -0
  244. package/tools/native-manual-projects.mjs +259 -0
  245. package/tools/native-manual.mjs +338 -0
  246. package/tools/native-model-picker-acceptance.py +306 -0
  247. package/tools/native-owned-runtime-env.sh +47 -0
  248. package/tools/native-paired-guards.mjs +282 -0
  249. package/tools/native-paired-manifest.mjs +72 -0
  250. package/tools/native-paired-orchestration.sh +51 -0
  251. package/tools/native-plugin-readiness.mjs +33 -0
  252. package/tools/native-plugin-readiness.test.mjs +53 -0
  253. package/tools/native-pty-invocation.mjs +26 -0
  254. package/tools/native-pty-runtime.mjs +83 -0
  255. package/tools/native-pty-smoke.py +147 -24
  256. package/tools/native-public-repo-shell-result.mjs +13 -0
  257. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  258. package/tools/native-runtime-identity.mjs +201 -0
  259. package/tools/native-slash-acceptance-env.py +38 -0
  260. package/tools/native-terminal-semantics.py +882 -0
  261. package/tools/owned-shell-observer/index.ts +16 -0
  262. package/tools/owned-shell-observer/package.json +1 -0
  263. package/tools/owned-visual-clear-env.py +48 -0
  264. package/tools/owned-visual-clear-model.py +21 -0
  265. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  266. package/tools/prepare-native-manual.mjs +122 -0
  267. package/tools/run-native-terminal-paired.sh +403 -0
  268. package/tools/runtime-token-env.mjs +8 -0
  269. package/tools/seed-native-switch-fixture.mjs +79 -0
  270. package/tools/test-native-controls.mjs +3 -1
  271. package/tools/test-native-daily-quests.mjs +65 -0
  272. package/tools/test-native-default-cli.mjs +106 -0
  273. package/tools/test-native-delivery-host.mjs +102 -20
  274. package/tools/test-native-evaluation.mjs +7 -3
  275. package/tools/test-native-glyph-pty.mjs +94 -0
  276. package/tools/test-native-host.mjs +91 -65
  277. package/tools/test-native-outcomes.mjs +3 -1
  278. package/tools/test-native-package.mjs +206 -38
  279. package/tools/test-native-performance.mjs +26 -0
  280. package/tools/test-native-product.mjs +90 -0
  281. package/tools/test-native-pty.mjs +34 -9
  282. package/tools/test-native-public-repo.mjs +224 -0
  283. package/tools/test-native-refresh-reentry.mjs +106 -0
  284. package/tools/test-native-report.mjs +27 -20
  285. package/tools/test-native-review.mjs +75 -0
  286. package/tools/test-native-runtime.mjs +120 -0
  287. package/tools/test-native-ship-models.mjs +176 -0
  288. package/tools/test-native-ship-walkthrough.mjs +96 -0
  289. package/tools/test-native-slash-commands.mjs +125 -0
  290. package/tools/test-native-slash-commands.py +242 -0
  291. package/tools/test-native-swe-host.mjs +224 -55
  292. package/tools/test-native-tasks.mjs +77 -0
  293. package/tools/test-native-terminal-performance.py +887 -0
  294. package/tools/test-native-terminal-readiness.sh +85 -0
  295. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  296. package/tools/test-native-visual-clear-installed.mjs +48 -0
  297. package/tools/test-owned-host-controls.mjs +238 -0
  298. package/tools/test-owned-opencode-runtime.mjs +35 -0
  299. package/tools/test-owned-visual-clear-env.py +120 -0
  300. package/tools/test-owned-visual-clear.py +373 -0
  301. package/tools/verify-draft-runtime-gate.mjs +138 -0
  302. package/tools/verify-owned-runtime-release.mjs +114 -0
  303. package/tools/verify-release-version.mjs +21 -1
@@ -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
+ }
@@ -8,7 +8,7 @@ type DeliveryState = Parameters<typeof startImplementation>[0]
8
8
  export const READ_ONLY_TOOLS = new Set(["read", "ls", "find", "grep", "code_nav", "delivery_probe"])
9
9
 
10
10
  export function requiresPlanApproval(tool: string) {
11
- return needsImplementation(tool)
11
+ return !["glob", "skill", "question"].includes(tool) && needsImplementation(tool.replace(/^carthagent_/, ""))
12
12
  }
13
13
 
14
14
  export function enforceToolPolicy(tool: string, state: DeliveryState, cwd: string) {
@@ -25,9 +25,10 @@ import {
25
25
  import { catalogModels } from '../../../../lib/providers/model-catalog.mjs';
26
26
  import { shipAgentDir } from '../../../../lib/ship-auth.mjs';
27
27
  import { createHash } from 'node:crypto';
28
- import { legacyOAuthForOpenCode, nativeCredentialFile, refreshNativeCredential, refreshSingleFlight } from '../../../../lib/opencode/credentials.mjs';
28
+ import { legacyOAuthForOpenCode, nativeCredentialFile, nativeCredentialDatabaseFile, readNativeOAuthCredential, refreshNativeCredential, refreshSingleFlight } from '../../../../lib/opencode/credentials.mjs';
29
29
  import { createDeviceAuthController } from './device-auth.ts';
30
30
  import { createSessionIdentity, isManagedShipCredential, shipTelemetryHeaders } from './telemetry.ts';
31
+ import { nativeLoginProvider } from '../../../../lib/opencode/provider-policy.mjs';
31
32
 
32
33
  const OAUTH_METHOD = 'carthagent-device';
33
34
  const SHIP_INTEGRATION_ID = EXPERIENTIAL_PROVIDER_ID;
@@ -97,14 +98,14 @@ function modelInfo(providerID: string, profile: CatalogModel, defaultBaseURL: st
97
98
 
98
99
  function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.Info['compatibility']> {
99
100
  const keys = [
100
- 'reasoningField', 'requireReasoning', 'maxTokensField', 'requireFinishReason',
101
+ 'reasoningField', 'requireReasoning', 'reasoningPlaceholder', 'maxTokensField', 'requireFinishReason',
101
102
  'requireAssistantAfterTool', 'supportsPromptCacheKey', 'cacheFormat',
102
103
  'supportsThinkingBlockBinding', 'supportsEffortUpdates',
103
104
  ] as const;
104
105
  return Object.fromEntries(keys.flatMap(key => key in compat ? [[key, compat[key]]] : [])) as NonNullable<Model.Info['compatibility']>;
105
106
  }
106
107
 
107
- function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow, rereadCredential, credentialFile }: {
108
+ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow, credentialFile, coordinationFile }: {
108
109
  env: NodeJS.ProcessEnv;
109
110
  fetchImpl: typeof fetch;
110
111
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
@@ -112,20 +113,25 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
112
113
  openBrowserImpl: typeof openBrowser;
113
114
  agentDir: string;
114
115
  startDeviceFlow: () => Promise<{ authorization: { userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; deviceCode: string }; callback: Promise<Credential.OAuth> }>;
115
- rereadCredential: () => Promise<Credential.OAuth | undefined>;
116
116
  credentialFile: string;
117
+ coordinationFile: string;
117
118
  }) {
118
119
  const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl, sleep: ms => sleep(ms) });
119
120
  return {
120
121
  integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID),
121
- method: { id: OAUTH_METHOD, type: 'oauth' as const, label: 'Sign in with Carthagent Ship' },
122
- authorize: async (_answer: Form.Answer) => {
123
- const legacy = await legacyOAuthForOpenCode({ agentDir });
122
+ method: {
123
+ id: OAUTH_METHOD, type: 'oauth' as const, label: 'Continue with Carthagent Cloud',
124
+ ...(env.CARTHAGENT_ADVANCED_AUTH === '1' ? { form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Cloud credential?', default: false }] satisfies Form.Fields } : {}),
125
+ },
126
+ authorize: async (answer: Form.Answer) => {
127
+ // The legacy file intentionally survives migration. Never let its presence
128
+ // short-circuit an explicit sign-in or recovery from revoked/old scopes.
129
+ const legacy = env.CARTHAGENT_ADVANCED_AUTH === '1' && answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
124
130
  if (legacy) {
125
131
  return {
126
132
  mode: 'auto' as const,
127
133
  url: 'https://api.carthagent.xyz',
128
- instructions: 'A legacy Carthagent Ship credential was found and will be migrated into OpenCode integration storage.',
134
+ instructions: 'A legacy Carthagent Cloud credential was found and will be migrated into native integration storage.',
129
135
  callback: Promise.resolve({
130
136
  type: 'oauth' as const,
131
137
  methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
@@ -140,30 +146,39 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
140
146
  Promise.resolve(openBrowserImpl(url)).catch(() => false);
141
147
  return {
142
148
  mode: 'auto' as const, url,
143
- instructions: `Approve device code ${response.userCode} in your browser.`,
149
+ instructions: `Sign in and connect this terminal using code ${response.userCode}. Check the account shown in your browser, then return here. If the code expires, start login again.`,
144
150
  expiresAt: now() + Number(response.expiresIn || 600) * 1000,
145
151
  callback,
146
152
  };
147
153
  },
148
154
  refresh: (credential: Credential.OAuth) => refreshSingleFlight(
149
- `${credential.methodID}:${credential.refresh}`,
150
- () => refreshNativeCredential({
151
- credential,
152
- credentialFile,
153
- reread: rereadCredential,
154
- refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
155
- ...oauthCredentials(await client.refresh({ refreshToken: current.refresh }), now()),
156
- ...(current.metadata ? { metadata: current.metadata } : {}) }),
157
- }),
155
+ `${coordinationFile}:${credential.methodID}:${credential.refresh}`,
156
+ async () => {
157
+ const credentialID = await readNativeOAuthCredential({ credentialFile,
158
+ integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID, refreshToken: credential.refresh });
159
+ return refreshNativeCredential({
160
+ credential,
161
+ credentialFile: coordinationFile,
162
+ credentialID,
163
+ reread: () => readNativeOAuthCredential({ credentialFile, credentialID,
164
+ integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID }),
165
+ refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
166
+ ...oauthCredentials(await client.refresh({ refreshToken: current.refresh, signal: AbortSignal.timeout(10_000) }), now()),
167
+ ...(current.metadata ? { metadata: current.metadata } : {}) }),
168
+ });
169
+ },
158
170
  ),
159
- label: () => 'Carthagent Ship',
171
+ label: () => 'Carthagent Cloud',
160
172
  };
161
173
  }
162
174
 
163
175
  function integrationDefinitions(editor: Parameters<Context['integration']['transform']>[0] extends (editor: infer E) => void ? E : never, options: Parameters<typeof oauthMethod>[0]) {
164
176
  editor.method.update(oauthMethod(options));
165
- editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Use an Experiential API key' } });
177
+ if (options.env.CARTHAGENT_ADVANCED_AUTH === '1') {
178
+ editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Advanced: use an Experiential API key' } });
179
+ }
166
180
  editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
181
+ editor.update(SHIP_INTEGRATION_ID, integration => { integration.name = EXPERIENTIAL_PROVIDER_NAME; });
167
182
  }
168
183
 
169
184
  function asModelInfo(providerID: string, model: { id: string }, baseURL: string) {
@@ -183,7 +198,7 @@ async function loadShipModels({ ctx, fetchImpl, env, baseURL, signal }: {
183
198
  const response = await fetchImpl(`${managed ? cloudGatewayUrl(env) : baseURL}/models`, {
184
199
  headers: { authorization: `Bearer ${key}` }, signal: signalWithTimeout(signal, DISCOVERY_TIMEOUT),
185
200
  });
186
- if (!response.ok) throw new Error(`Carthagent Ship model discovery failed (${response.status}).`);
201
+ if (!response.ok) throw new Error(`Carthagent Cloud model discovery failed (${response.status}).`);
187
202
  const models = parseExperientialModels(await response.json()).slice(0, PROFILE_CAP);
188
203
  if (connection && managed) return { connection, models, managed };
189
204
  return { connection, models, managed };
@@ -199,6 +214,7 @@ interface ProviderRegistrationOptions {
199
214
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
200
215
  agentDir?: string;
201
216
  credentialFile?: string;
217
+ vaultFile?: string;
202
218
  openBrowser?: typeof openBrowser;
203
219
  telemetryIdentity?: ReturnType<typeof createSessionIdentity>;
204
220
  sessionIdentity?: (event: SessionModelRequest) => string;
@@ -215,12 +231,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
215
231
  const agentDir = options.agentDir ?? shipAgentDir(env);
216
232
  const openBrowserImpl = options.openBrowser ?? openBrowser;
217
233
  const deviceAuth = options.deviceAuth ?? createDeviceAuthController({ env, fetchImpl, agentDir, now, sleep });
218
- const rereadCredential = async (): Promise<Credential.OAuth | undefined> => {
219
- const active = await ctx.integration.connection.active(SHIP_INTEGRATION_ID);
220
- if (!active) return undefined;
221
- const value = await ctx.integration.connection.resolve(active);
222
- return value?.type === 'oauth' && value.methodID === OAUTH_METHOD ? value : undefined;
223
- };
234
+ // credentialFile historically selects the coordination namespace. Retain
235
+ // the fixture shorthand (one explicit path for both) unless vaultFile is set.
236
+ const coordinationFile = options.credentialFile ?? nativeCredentialFile(env);
237
+ const credentialFile = options.vaultFile ?? options.credentialFile ?? nativeCredentialDatabaseFile(env, ctx.app.channel);
224
238
  const requestIdentity = options.sessionIdentity;
225
239
  const identity = options.telemetryIdentity ?? createSessionIdentity();
226
240
  const requestSecret = options.requestSecret ?? `${ctx.location?.project?.id || ctx.location?.directory || 'ctg'}:request-v1`;
@@ -236,23 +250,36 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
236
250
  return failures;
237
251
  };
238
252
  try {
253
+ const excludedIntegrations = new Set<string>();
239
254
  const integration = await ctx.integration.transform(editor => {
240
255
  integrationDefinitions(editor, {
241
256
  env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow: deviceAuth.start,
242
- rereadCredential,
243
- credentialFile: options.credentialFile ?? nativeCredentialFile(env),
257
+ credentialFile,
258
+ coordinationFile,
244
259
  });
260
+ for (const integration of editor.list()) {
261
+ const supported = nativeLoginProvider(integration.id);
262
+ if (supported) editor.update(integration.id, item => { item.name = supported.name; });
263
+ else {
264
+ excludedIntegrations.add(integration.id);
265
+ editor.remove(integration.id);
266
+ }
267
+ }
245
268
  });
246
269
  registrations.push(integration);
247
270
 
248
271
  const baseURL = experientialBaseUrl(env);
249
- const shipModels: Model.Info[] = [];
272
+ let shipModels: Model.Info[] = [];
273
+ let shipConnection: ConnectionInfo | undefined;
250
274
  const subscriptions: Record<string, Model.Info[]> = { [ANTHROPIC_PROVIDER_ID]: [], [OPENAI_CODEX_PROVIDER_ID]: [] };
251
275
  const subscriptionConnections: Partial<Record<string, ConnectionInfo>> = {};
252
276
  const providerRegistration = await ctx.provider.transform(editor => {
277
+ for (const { provider } of editor.list()) {
278
+ if (provider.id.startsWith('opencode') || excludedIntegrations.has(provider.integrationID ?? provider.id)) editor.remove(provider.id);
279
+ }
253
280
  editor.add({ info: providerInfo(EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
254
281
  '@opencode/ai/providers/openai-compatible', SHIP_INTEGRATION_ID,
255
- { baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels });
282
+ { baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels, sourceConnection: shipConnection });
256
283
  // Subscription IDs already exist in the native catalog. Overlay only the
257
284
  // definitions and retain catalog cost/capability metadata.
258
285
  for (const [providerID, baseURL] of [
@@ -330,8 +357,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
330
357
  const refresh = async (signal?: AbortSignal): Promise<void> => {
331
358
  const inventory = await loadShipModels({ ctx, fetchImpl, env, baseURL, signal });
332
359
  if (inventory?.models) {
333
- shipModels.splice(0, shipModels.length, ...inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
334
- inventory.managed ? cloudGatewayUrl(env) : baseURL)));
360
+ shipConnection = inventory.connection;
361
+ // The host indexes immutable model arrays by identity.
362
+ shipModels = inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
363
+ inventory.managed ? cloudGatewayUrl(env) : baseURL));
335
364
  await ctx.provider.reload();
336
365
  }
337
366
  for (const providerID of [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]) {
@@ -340,7 +369,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
340
369
  if (!active) {
341
370
  if (subscriptionConnections[providerID]) {
342
371
  delete subscriptionConnections[providerID];
343
- subscriptions[providerID].splice(0, subscriptions[providerID].length);
372
+ subscriptions[providerID] = [];
344
373
  await ctx.provider.reload();
345
374
  }
346
375
  continue;
@@ -357,7 +386,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
357
386
  const accountMetadata = credential && 'metadata' in credential ? credential.metadata as Record<string, unknown> | undefined : undefined;
358
387
  const accountKey = createHash('sha256').update(String(('id' in active ? active.id : '') || accountMetadata?.accountId || (credential?.type === 'oauth' ? credential.access : credential?.type === 'key' ? credential.key : '') || '')).digest('hex');
359
388
  if (prior?.account !== accountKey) {
360
- subscriptions[providerID].splice(0, subscriptions[providerID].length);
389
+ subscriptions[providerID] = [];
361
390
  }
362
391
  const known = new Map([
363
392
  ...catalogModels(providerID).map((model: CatalogModel) => [model.id, model] as const),
@@ -369,7 +398,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
369
398
  ? anthropicModelDefinition(model, base) : codexModelDefinition(model, base),
370
399
  providerID === ANTHROPIC_PROVIDER_ID ? 'https://api.anthropic.com' : 'https://chatgpt.com/backend-api');
371
400
  });
372
- subscriptions[providerID].splice(0, subscriptions[providerID].length, ...converted);
401
+ subscriptions[providerID] = converted;
373
402
  subscriptionConnections[providerID] = active;
374
403
  await ctx.storage.set(cacheKey, { account: accountKey, models: discovered, checkedAt: now() });
375
404
  await ctx.provider.reload();
@@ -1,6 +1,7 @@
1
1
  import type { Context } from '@opencode/plugin/promise/plugin';
2
- import { framePrompt, shouldFrame, unframe } from '../../../../lib/tui/framing.mjs';
2
+ import { framePrompt, shouldFrame, unframe } from './framing.mjs';
3
3
  import { SessionControls } from '../session-rpc';
4
+ import { isShipAgent } from './ship-agents';
4
5
  import { effectiveSessionOptions } from '../../../../lib/opencode/session-options.mjs';
5
6
 
6
7
  /** Admission framing is session-scoped; raw bypasses framing, never delivery policy. */
@@ -25,6 +26,7 @@ export async function registerSessionControls(ctx: Context) {
25
26
  const prompt = await ctx.session.hook('prompt', async (event) => {
26
27
  const session = await ctx.session.get({ sessionID: event.sessionID });
27
28
  const options = effectiveSessionOptions(session, ctx.options);
29
+ if (!isShipAgent(session.agent)) return;
28
30
  const saved = await settings(event.sessionID);
29
31
  if (event.metadata?.['ctg.raw'] === true || event.metadata?.['ctg.framed'] === true) return;
30
32
  if (!shouldFrame(event.prompt.text, {
@@ -0,0 +1,77 @@
1
+ import type { Context } from "@opencode/plugin/promise/plugin"
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs"
3
+ import { dirname, join, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ export const SHIP_AGENT_ID = "ship"
7
+ export const LITE_AGENT_ID = "lite"
8
+
9
+ const SKILLS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../skills")
10
+
11
+ /**
12
+ * The delivery harness runs only for Ship. A missing agent ID means the host could not
13
+ * identify the mode, so enforcement stays on rather than silently disappearing.
14
+ */
15
+ export function isShipAgent(agent: string | undefined | null) {
16
+ return agent === undefined || agent === null || agent === "" || agent === SHIP_AGENT_ID
17
+ }
18
+
19
+ type AgentEditor = Parameters<Parameters<Context["agent"]["transform"]>[0]>[0]
20
+ type SkillEditor = Parameters<Parameters<Context["skill"]["transform"]>[0]>[0]
21
+
22
+ /**
23
+ * Ship deliberately sets no `system` override: the host keeps its native base prompt and the
24
+ * delivery plugin injects the workflow per request, as the legacy extension appended it.
25
+ */
26
+ export async function registerShipAgents(ctx: Pick<Context, "agent">) {
27
+ const registration = await ctx.agent.transform((editor: AgentEditor) => {
28
+ editor.update(SHIP_AGENT_ID, item => {
29
+ item.name = "Ship" as unknown as typeof item.name
30
+ item.description = "Carthagent delivery workflow: acceptance contract, implementation, fresh check evidence, review and verified handoff."
31
+ item.mode = "primary"
32
+ item.system = undefined
33
+ item.color = "#e6c480"
34
+ item.permissions.push({ action: "question", resource: "*", effect: "allow" })
35
+ })
36
+ editor.update(LITE_AGENT_ID, item => {
37
+ item.name = "Lite" as unknown as typeof item.name
38
+ item.description = "Direct execution with existing skills and tools. No delivery harness."
39
+ item.mode = "primary"
40
+ item.color = "#b89ce0"
41
+ item.system = "You are a direct execution agent. Use the skills and tools already available and work the task directly. No extra planning ceremony, no harness phases — just do the work well and concisely."
42
+ item.permissions.push({ action: "question", resource: "*", effect: "allow" })
43
+ })
44
+ editor.default(SHIP_AGENT_ID)
45
+ })
46
+ return async () => { await registration.dispose() }
47
+ }
48
+
49
+ function frontmatter(raw: string) {
50
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
51
+ if (!match) return { fields: {} as Record<string, string>, body: raw }
52
+ const fields: Record<string, string> = {}
53
+ for (const line of match[1].split(/\r?\n/)) {
54
+ const field = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/)
55
+ if (field) fields[field[1]] = field[2].trim().replace(/^["']|["']$/g, "")
56
+ }
57
+ return { fields, body: raw.slice(match[0].length) }
58
+ }
59
+
60
+ export function bundledSkills(root = SKILLS_ROOT) {
61
+ if (!existsSync(root)) return []
62
+ return readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory()).flatMap(entry => {
63
+ const path = join(root, entry.name, "SKILL.md")
64
+ if (!existsSync(path)) return []
65
+ const { fields, body } = frontmatter(readFileSync(path, "utf8"))
66
+ return [{ id: entry.name, name: fields.name || entry.name, ...(fields.description ? { description: fields.description } : {}), path, content: body }]
67
+ })
68
+ }
69
+
70
+ /** Legacy runs passed `--skill skills/`; the native host receives the same skill set. */
71
+ export async function registerShipSkills(ctx: Pick<Context, "skill">, root = SKILLS_ROOT) {
72
+ const skills = bundledSkills(root)
73
+ const registration = await ctx.skill.transform((editor: SkillEditor) => {
74
+ for (const skill of skills) if (!editor.get(skill.id)) editor.add(skill as Parameters<SkillEditor["add"]>[0])
75
+ })
76
+ return async () => { await registration.dispose() }
77
+ }
@@ -0,0 +1,60 @@
1
+ import { archiveContext } from '../../../../lib/context-archive.mjs';
2
+
3
+ const DELIVERY = new Set(['delivery_plan', 'delivery_revise', 'delivery_design', 'delivery_status', 'delivery_progress', 'delivery_check', 'delivery_review']);
4
+ export const localToolName = name => String(name).replace(/^carthagent[._]/, '');
5
+ const blocking = value => /"severity"\s*:\s*"blocking"|"passed"\s*:\s*false/.test(JSON.stringify(value).replaceAll('\\"', '"'));
6
+
7
+ // Native results can group multiple calls in one message. Never split a call/result
8
+ // pair or discard other content on the assistant/result messages when archiving.
9
+ export function archiveDeliveryExchanges(messages, { cwd, directory, allow = false, keepRecent = 2 }) {
10
+ if (!allow) return { messages, failed: false };
11
+ const groups = [];
12
+ for (let i = 0; i < messages.length; i++) {
13
+ const message = messages[i];
14
+ if (message.role !== 'assistant' || !message.content?.length || message.content.some(part => part.type !== 'tool-call' || !DELIVERY.has(localToolName(part.name)))) continue;
15
+ const ids = new Set(message.content.map(part => part.id));
16
+ if (ids.has(undefined) || ids.size !== message.content.length) continue;
17
+ let end = i + 1;
18
+ const results = [];
19
+ while (end < messages.length && messages[end].role === 'tool') {
20
+ results.push(...messages[end].content);
21
+ end++;
22
+ if (results.length >= ids.size) break;
23
+ }
24
+ if (results.length !== ids.size || results.some(part => part.type !== 'tool-result' || !ids.has(part.id) || part.result?.type === 'error' || blocking(part.result))) continue;
25
+ if (new Set(results.map(part => part.id)).size !== ids.size) continue;
26
+ groups.push({ start: i, end, tools: message.content.map(part => part.name) });
27
+ i = end - 1;
28
+ }
29
+ const replace = new Map(); let failed = false;
30
+ for (const group of groups.slice(0, Math.max(0, groups.length - keepRecent))) {
31
+ try {
32
+ const archive = archiveContext(cwd, directory, messages.slice(group.start, group.end));
33
+ replace.set(group.start, { ...group, archive });
34
+ } catch { failed = true; }
35
+ }
36
+ const projected = [];
37
+ for (let i = 0; i < messages.length;) {
38
+ const group = replace.get(i);
39
+ if (!group) { projected.push(messages[i++]); continue; }
40
+ projected.push({ role: 'user', content: [{ type: 'text', text: `Archived successful delivery exchange (not proof of correctness). Tools: ${group.tools.join(', ')}. Read ${group.archive.path} for original messages; sha256=${group.archive.digest}. Current obligations/failures are in the delivery recovery record.` }] });
41
+ i = group.end;
42
+ }
43
+ return { messages: projected, failed };
44
+ }
45
+
46
+ export function normalizeDesignReview(input) {
47
+ const review = input.review ?? { challenges: input.challenges, walkthroughs: input.walkthroughs, findings: input.findings, limitations: input.limitations };
48
+ return { ...review, captureId: review.captureId ?? input.captureId ?? review.id };
49
+ }
50
+
51
+ export function shellProtection(command, initialFiles) {
52
+ const broad = /\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)|\btaskkill\b[^\n]*(?:\/im|\*)|\bstop-process\b[^\n]*\s-name\b|\bget-process\b[^\n]*\|\s*stop-process\b/i;
53
+ if (broad.test(command)) return 'Broad process termination is blocked. Stop only the specific PID/process you started.';
54
+ for (const file of initialFiles) {
55
+ const escaped = file.split(/[\\/]/).at(-1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ if (new RegExp(`(?:>|tee\\s+)\\s*["']?[^\\s;|]*${escaped}(?:["'\\s;|]|$)`).test(command)) return 'Shell redirection cannot overwrite an existing source file. Use a focused edit.';
57
+ if (/writeFile|write_text|write_bytes/.test(command) && command.includes(file.split(/[\\/]/).at(-1))) return 'A script cannot replace an existing source file. Use a focused edit.';
58
+ if (new RegExp(`(?:^|[;&|]\\s*)(?:rm|mv|cp)\\s+[^;|]*${escaped}(?:["'\\s;|]|$)`).test(command)) return 'Shell commands cannot remove or replace an existing source file. Use a focused edit.';
59
+ }
60
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync } from "node:fs"
2
+ import { fileURLToPath } from "node:url"
3
+ import { DELIVERY_INVARIANTS, TEST_INFRASTRUCTURE_GUIDANCE, deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
4
+ import { PLANNING_START } from "../../../../lib/planning-guide.mjs"
5
+ import { formatGuidance } from "../../../../lib/guidance.mjs"
6
+ import { looksInformational } from "../../../../lib/delivery.mjs"
7
+ import { unframe } from "./framing.mjs"
8
+
9
+ export const STANDARD_GUIDANCE = "Standard delivery: inspect relevant files (glob/read/code_nav); create delivery_plan with complete requirements, artifact roots, a few steps and real checks. A valid required plan enables implementation immediately: design components/scenario ceremony is optional. Revisions merge design/workflow fields, replace supplied arrays, preserve obligations and require fresh final evidence, not repeated approval. Mark implementation steps done independently of integrated check execution. Use delivery_check all, final delivery_review inspect/record, then delivery_finish with honest limitations. No fabricated evidence, scope reduction or unrequested dependency changes. Informational plans do not authorize implementation. For bounded environment versions use delivery_probe kind=node-version or npm-version; inspect other APIs through read."
10
+
11
+ export const TOOL_SURFACE_GUIDANCE = "Read files with direct read/glob/grep tools. Ship withholds direct write/edit/shell tools until implementation is authorized: first create delivery_plan with a valid required acceptance contract; standard mode then unlocks implementation on the next tool turn, while strict mode also requires tested-design approval. File edits and commands use those direct tools, including shell (not bash), outside execute. The code-mode search() catalog lists only namespaced tools.* helpers, not direct tools. If write/edit/shell are missing, complete the required delivery step instead of searching catalogs or runtime internals for alternatives. Use delivery_probe for bounded pre-plan version discovery; never use delivery_check to bootstrap file edits."
12
+
13
+ const browserCheck = fileURLToPath(new URL("../../../../tools/browser-check.mjs", import.meta.url))
14
+ const browserGuidance = existsSync(browserCheck)
15
+ ? `For web/browser tasks, a self-contained browser check is available: ${JSON.stringify(browserCheck)}. Declare it as a runtime check, e.g. ["node",${JSON.stringify(browserCheck)},"--page","index.html","--assert","#app","--assert-count","#items:3","--click","#btn","--then-text","#out:done","--console-clean","--screenshot","artifacts/ui.png"]. It serves the workspace over HTTP, runs jsdom assertions with real inline-script execution (clicks, text, selectors, console-error detection), and captures a real screenshot with the bundled lightweight headless browser (auto-downloaded on first use).`
16
+ : ""
17
+
18
+ export function originalTask(prompt: string) { return unframe(prompt) ?? prompt }
19
+
20
+ /* eslint-disable @typescript-eslint/no-explicit-any -- Host-neutral domain snapshots are JS objects. */
21
+ export function shipGuidance({ state, workflow, prompt = "", profiles = [], discovery, extraGuidance, required = [], imageInput }: {
22
+ state: any; workflow: string; prompt?: string; profiles?: any[]; discovery?: any;
23
+ extraGuidance?: string; required?: any[]; imageInput?: boolean;
24
+ }) {
25
+ /* eslint-enable @typescript-eslint/no-explicit-any */
26
+ const task = originalTask(prompt)
27
+ const mode = state && !["verified", "blocked"].includes(state.status) ? state.workflowMode : workflow
28
+ const parts = mode === "standard"
29
+ ? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE, TOOL_SURFACE_GUIDANCE]
30
+ : [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state), TOOL_SURFACE_GUIDANCE]
31
+ if (/\b(test|typescript|route|backend|api)\b/i.test(task)) parts.push(TEST_INFRASTRUCTURE_GUIDANCE)
32
+ if (profiles.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(task)) parts.push(browserGuidance)
33
+ if (imageInput === false) parts.push("This session's model cannot process images: do not attempt to view, read, or reason over image files or screenshots — visual review is unavailable. Verify visual behavior through DOM structure, computed styles, and console evidence instead; screenshots remain useful as evidence artifacts for the user even though you cannot inspect them.")
34
+ if (discovery?.active && mode !== "standard") parts.push(PLANNING_START)
35
+ const routed = formatGuidance(profiles)
36
+ if (routed) parts.push(routed)
37
+ if (extraGuidance) parts.push(`Task-specific delivery context:\n${extraGuidance}`)
38
+ if (required.length) parts.push(`User-owned required validators will be added to your plan automatically: ${JSON.stringify(required)}. Run delivery_check id="all"; repair failures rather than replacing or bypassing these checks.`)
39
+ if (looksInformational(task)) parts.push('Receipt check: this prompt reads as informational/read-only. Answer it directly, or if you record a contract use delivery_plan verification:"none" (no checks) or "advisory" (optional non-blocking evidence). Do not enter the verify/repair loop for a pure question — decide the classification now, before the verify loop.')
40
+ return parts.filter(Boolean).join("\n\n")
41
+ }
@@ -0,0 +1,71 @@
1
+ import { CloudClient, cloudControlUrl } from '../../../../lib/cloud/client.mjs';
2
+ import type { Context } from '@opencode/plugin/promise/plugin';
3
+ import type { ConnectionInfo } from '@opencode/client';
4
+
5
+ type ShipContext = Pick<Context, 'integration'>;
6
+ type ActiveCredential = Extract<ConnectionInfo, { type: 'credential' }>;
7
+ type CloudClientPort = Pick<CloudClient, 'account' | 'usage' | 'quote' | 'checkout' | 'portal' | 'revokeCurrent' | 'revokeSession'>;
8
+
9
+ /** Create profile-scoped Ship RPC handlers; injection keeps cloud behavior testable offline. */
10
+ export function createShipRpcHandlers(ctx: ShipContext, options: {
11
+ cloudClient?: CloudClientPort;
12
+ baseUrl?: string;
13
+ cloudUrl?: string;
14
+ } = {}) {
15
+ const cloud = options.cloudClient ?? new CloudClient({ baseUrl: options.baseUrl ?? cloudControlUrl() });
16
+ async function access() {
17
+ const active = await ctx.integration.connection.active('experiential-labs');
18
+ if (!active || active.type !== 'credential' || active.method !== 'oauth') throw new Error('An active Carthagent Cloud OAuth connection is required.');
19
+ const credential = await ctx.integration.connection.resolve(active);
20
+ if (credential?.type !== 'oauth' || credential.methodID !== 'carthagent-device' || !credential.access) {
21
+ throw new Error('An active Carthagent Cloud OAuth connection is required.');
22
+ }
23
+ return { active: active as ActiveCredential, token: credential.access };
24
+ }
25
+ return {
26
+ shipAccount: async (_input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipAccount>) => {
27
+ call.signal.throwIfAborted();
28
+ let token: string;
29
+ try { ({ token } = await access()); }
30
+ catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
31
+ return { account: await cloud.account({ accessToken: token, signal: call.signal }) };
32
+ },
33
+ shipUsage: async (input: unknown, call: { signal: AbortSignal }) => {
34
+ call.signal.throwIfAborted();
35
+ const { token } = await access();
36
+ const requested = (input as { limit?: number }).limit ?? 50;
37
+ if (!Number.isSafeInteger(requested) || requested < 1 || requested > 200) throw new Error('Cloud usage limit must be between 1 and 200.');
38
+ return { usage: await cloud.usage({ accessToken: token, limit: requested, signal: call.signal } as Parameters<CloudClient['usage']>[0]) };
39
+ },
40
+ shipBilling: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipBilling>) => {
41
+ call.signal.throwIfAborted();
42
+ let token: string;
43
+ try { ({ token } = await access()); }
44
+ catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
45
+ const { action, plan = 'builder' } = input as { action: 'checkout' | 'portal' | 'quote'; plan?: string };
46
+ if (!['checkout', 'portal', 'quote'].includes(action)) throw new Error('Unsupported Cloud billing action.');
47
+ if (!['builder', 'pro', 'max'].includes(plan)) throw new Error('Choose Builder, Pro, or Max.');
48
+ if (action === 'quote') return { quote: await cloud.quote({ accessToken: token, plan, signal: call.signal }) };
49
+ const returnUrl = `${options.cloudUrl ?? cloudControlUrl()}/account`;
50
+ const result = action === 'portal'
51
+ ? await cloud.portal({ accessToken: token, returnUrl, signal: call.signal })
52
+ : await cloud.checkout({ accessToken: token, priceId: plan, successUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/complete`, cancelUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/cancelled?plan=${plan}`, signal: call.signal });
53
+ return { url: result.url };
54
+ },
55
+ shipLogout: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipLogout>) => {
56
+ call.signal.throwIfAborted();
57
+ let active: ActiveCredential;
58
+ let token: string;
59
+ try { ({ active, token } = await access()); }
60
+ catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
61
+ const session = (input as { session?: string }).session;
62
+ if (session) {
63
+ await cloud.revokeSession({ accessToken: token, sessionId: session, signal: call.signal });
64
+ return { revoked: true };
65
+ }
66
+ await cloud.revokeCurrent({ accessToken: token, signal: call.signal });
67
+ await ctx.integration.transform((editor) => editor.remove(active.id));
68
+ return { revoked: true, credentialID: active.id };
69
+ },
70
+ };
71
+ }