carthagent 1.0.7 → 1.1.0-preview.13

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 (289) hide show
  1. package/README.md +140 -83
  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 +336 -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-combined-v5.patch +13965 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  58. package/docs/release-notes-native-draft.md +73 -0
  59. package/docs/release-notes-preview.10.md +121 -0
  60. package/docs/release-notes-preview.11.md +76 -0
  61. package/docs/release-notes-preview.12.md +55 -0
  62. package/docs/release-notes-preview.13.md +68 -0
  63. package/docs/release-notes-preview.9.md +130 -0
  64. package/docs/source-preview.md +120 -0
  65. package/evaluate.mjs +28 -1
  66. package/lib/budget.mjs +1 -1
  67. package/lib/codegraph.mjs +1 -1
  68. package/lib/delivery-review.mjs +11 -1
  69. package/lib/delivery.mjs +3 -2
  70. package/lib/environment.mjs +8 -0
  71. package/lib/guidance.mjs +1 -1
  72. package/lib/opencode/cli-runtime.mjs +1171 -0
  73. package/lib/opencode/cli.mjs +252 -0
  74. package/lib/opencode/credentials.mjs +63 -8
  75. package/lib/opencode/demo.mjs +53 -0
  76. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  77. package/lib/opencode/evaluation.mjs +32 -12
  78. package/lib/opencode/headless.mjs +8 -4
  79. package/lib/opencode/provider-policy.mjs +23 -0
  80. package/lib/opencode/review.mjs +37 -16
  81. package/lib/opencode/runtime-artifact.mjs +335 -0
  82. package/lib/opencode/runtime-github-transport.mjs +121 -0
  83. package/lib/opencode/runtime-release.json +36 -0
  84. package/lib/opencode/runtime.mjs +299 -20
  85. package/lib/opencode/service.mjs +77 -35
  86. package/lib/opencode/session-options.mjs +6 -2
  87. package/lib/opencode/update.mjs +222 -0
  88. package/lib/opencode/workspace.mjs +1 -1
  89. package/lib/planning-advice.mjs +2 -2
  90. package/lib/providers/experiential.mjs +2 -2
  91. package/lib/tui/framing.mjs +1 -80
  92. package/lib/update.mjs +10 -1
  93. package/package.json +33 -7
  94. package/plugins/carthagent/README.md +23 -0
  95. package/plugins/carthagent/src/bootstrap.ts +1 -1
  96. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  97. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  98. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  99. package/plugins/carthagent/src/index.ts +71 -24
  100. package/plugins/carthagent/src/rpc.ts +51 -6
  101. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  102. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  103. package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
  104. package/plugins/carthagent/src/server/delivery.ts +227 -112
  105. package/plugins/carthagent/src/server/framing.mjs +80 -0
  106. package/plugins/carthagent/src/server/policy.ts +1 -1
  107. package/plugins/carthagent/src/server/providers.ts +59 -31
  108. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  109. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  110. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  111. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  112. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  113. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  114. package/plugins/carthagent/src/testing.ts +5 -0
  115. package/plugins/carthagent/src/tui.tsx +496 -0
  116. package/plugins/carthagent/src/ui/client.ts +125 -0
  117. package/plugins/carthagent/src/ui/commands.ts +70 -0
  118. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  119. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  120. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  121. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  122. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  123. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  124. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  125. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  126. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  127. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  128. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  129. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  130. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  131. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  132. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  133. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  134. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  135. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  136. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  137. package/plugins/carthagent/src/ui/voyages.tsx +129 -0
  138. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  139. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  140. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  141. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  142. package/plugins/carthagent/tests/commands.test.ts +107 -0
  143. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  144. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  145. package/plugins/carthagent/themes/carthage.json +67 -0
  146. package/plugins/carthagent/tui.ts +26 -1
  147. package/script/ctg/build-owned-runtime.sh +166 -0
  148. package/tests/budget-engine.test.mjs +4 -3
  149. package/tests/cli.test.mjs +52 -96
  150. package/tests/daily-evidence.test.mjs +62 -0
  151. package/tests/delivery-review.test.mjs +11 -0
  152. package/tests/delivery.test.mjs +22 -1
  153. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  154. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  155. package/tests/installed-runtime-contract.test.mjs +43 -0
  156. package/tests/native-cli-host-smoke.mjs +457 -0
  157. package/tests/native-daily-editor-focus.test.mjs +213 -0
  158. package/tests/native-daily-editor.test.mjs +43 -0
  159. package/tests/native-daily-quests-layout.test.mjs +53 -0
  160. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  161. package/tests/native-daily-quests-ui.test.mjs +778 -0
  162. package/tests/native-daily-source.test.mjs +451 -0
  163. package/tests/native-demo-ui.test.mjs +48 -0
  164. package/tests/native-evaluation-admission.test.mjs +24 -0
  165. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  166. package/tests/native-fixture-runtime.test.mjs +111 -0
  167. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  168. package/tests/native-glyph-pty.test.mjs +107 -0
  169. package/tests/native-manual-projects.test.mjs +170 -0
  170. package/tests/native-model-command.test.mjs +67 -0
  171. package/tests/native-owned-runtime-env.test.mjs +140 -0
  172. package/tests/native-paired-guards.test.mjs +464 -0
  173. package/tests/native-provider-policy.test.mjs +31 -0
  174. package/tests/native-pty-invocation.test.mjs +114 -0
  175. package/tests/native-pty-runtime.test.mjs +45 -0
  176. package/tests/native-resume-session.test.mjs +46 -0
  177. package/tests/native-review-consumer.test.mjs +286 -0
  178. package/tests/native-runtime-identity.test.mjs +214 -0
  179. package/tests/native-select-mouse.test.mjs +23 -0
  180. package/tests/native-ship-parity.test.mjs +526 -0
  181. package/tests/native-ship-plan.test.mjs +85 -0
  182. package/tests/native-ship-rpc.test.mjs +71 -0
  183. package/tests/native-ui-commands.test.mjs +6 -0
  184. package/tests/native-voyages-layout.test.mjs +28 -0
  185. package/tests/native-voyages-navigation.test.mjs +169 -0
  186. package/tests/native-voyages-rpc.test.mjs +395 -0
  187. package/tests/native-voyages-ui.test.mjs +277 -0
  188. package/tests/opencode-atomic-write.test.mjs +55 -0
  189. package/tests/opencode-cli.test.mjs +1312 -0
  190. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  191. package/tests/opencode-delivery.test.mjs +16 -0
  192. package/tests/opencode-providers.test.mjs +245 -18
  193. package/tests/opencode-runtime.test.mjs +546 -8
  194. package/tests/opencode-session-controls.test.mjs +15 -0
  195. package/tests/opencode-ship-rpc.test.mjs +71 -0
  196. package/tests/opencode-update.test.mjs +475 -0
  197. package/tests/opencode-workspace.test.mjs +51 -2
  198. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  199. package/tests/release-workflow.test.mjs +46 -0
  200. package/tests/runtime-artifact.test.mjs +269 -0
  201. package/tests/runtime-development-trust.test.mjs +230 -0
  202. package/tests/runtime-github-transport.test.mjs +132 -0
  203. package/tests/runtime-token-env.test.mjs +23 -0
  204. package/tests/server.test.mjs +3 -1
  205. package/tests/ship-telemetry.test.mjs +1 -1
  206. package/tests/source-preview.test.mjs +253 -0
  207. package/tests/test_native_terminal_probe.py +76 -0
  208. package/tests/test_native_terminal_semantics.py +232 -0
  209. package/tests/update.test.mjs +42 -5
  210. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  211. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  212. package/tests/workspace-live.test.mjs +127 -0
  213. package/tools/benchmark/diagnose-request.mjs +59 -36
  214. package/tools/benchmark/eval-common.mjs +3 -3
  215. package/tools/benchmark/evaluators.test.mjs +4 -4
  216. package/tools/benchmark/guard.test.mjs +35 -2
  217. package/tools/benchmark/guard.ts +20 -7
  218. package/tools/benchmark/planning-startup.test.mjs +30 -34
  219. package/tools/benchmark/runner.mjs +12 -7
  220. package/tools/benchmark/runner.test.mjs +23 -7
  221. package/tools/benchmark/upstream-error.test.mjs +16 -2
  222. package/tools/generate-owned-runtime-release.mjs +217 -0
  223. package/tools/installed-runtime-contract.mjs +16 -0
  224. package/tools/manual-runtime-manifest.mjs +39 -0
  225. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  226. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  227. package/tools/native-fixture.mjs +199 -40
  228. package/tools/native-glyph-helpers.py +127 -0
  229. package/tools/native-glyph-pty.py +144 -0
  230. package/tools/native-manual-projects.mjs +259 -0
  231. package/tools/native-manual.mjs +338 -0
  232. package/tools/native-model-picker-acceptance.py +306 -0
  233. package/tools/native-owned-runtime-env.sh +47 -0
  234. package/tools/native-paired-guards.mjs +282 -0
  235. package/tools/native-paired-manifest.mjs +72 -0
  236. package/tools/native-paired-orchestration.sh +51 -0
  237. package/tools/native-plugin-readiness.mjs +33 -0
  238. package/tools/native-plugin-readiness.test.mjs +53 -0
  239. package/tools/native-pty-invocation.mjs +26 -0
  240. package/tools/native-pty-runtime.mjs +83 -0
  241. package/tools/native-pty-smoke.py +147 -24
  242. package/tools/native-public-repo-shell-result.mjs +13 -0
  243. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  244. package/tools/native-runtime-identity.mjs +201 -0
  245. package/tools/native-slash-acceptance-env.py +38 -0
  246. package/tools/native-terminal-semantics.py +876 -0
  247. package/tools/owned-shell-observer/index.ts +16 -0
  248. package/tools/owned-shell-observer/package.json +1 -0
  249. package/tools/owned-visual-clear-env.py +48 -0
  250. package/tools/owned-visual-clear-model.py +21 -0
  251. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  252. package/tools/prepare-native-manual.mjs +122 -0
  253. package/tools/run-native-terminal-paired.sh +403 -0
  254. package/tools/runtime-token-env.mjs +8 -0
  255. package/tools/seed-native-switch-fixture.mjs +79 -0
  256. package/tools/test-native-controls.mjs +3 -1
  257. package/tools/test-native-daily-quests.mjs +65 -0
  258. package/tools/test-native-default-cli.mjs +106 -0
  259. package/tools/test-native-delivery-host.mjs +102 -20
  260. package/tools/test-native-evaluation.mjs +7 -3
  261. package/tools/test-native-glyph-pty.mjs +94 -0
  262. package/tools/test-native-host.mjs +87 -63
  263. package/tools/test-native-outcomes.mjs +3 -1
  264. package/tools/test-native-package.mjs +206 -38
  265. package/tools/test-native-performance.mjs +26 -0
  266. package/tools/test-native-product.mjs +90 -0
  267. package/tools/test-native-pty.mjs +34 -9
  268. package/tools/test-native-public-repo.mjs +224 -0
  269. package/tools/test-native-refresh-reentry.mjs +106 -0
  270. package/tools/test-native-report.mjs +27 -20
  271. package/tools/test-native-review.mjs +75 -0
  272. package/tools/test-native-runtime.mjs +120 -0
  273. package/tools/test-native-ship-models.mjs +176 -0
  274. package/tools/test-native-ship-walkthrough.mjs +96 -0
  275. package/tools/test-native-slash-commands.mjs +125 -0
  276. package/tools/test-native-slash-commands.py +242 -0
  277. package/tools/test-native-swe-host.mjs +224 -55
  278. package/tools/test-native-tasks.mjs +77 -0
  279. package/tools/test-native-terminal-performance.py +887 -0
  280. package/tools/test-native-terminal-readiness.sh +85 -0
  281. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  282. package/tools/test-native-visual-clear-installed.mjs +48 -0
  283. package/tools/test-owned-host-controls.mjs +238 -0
  284. package/tools/test-owned-opencode-runtime.mjs +35 -0
  285. package/tools/test-owned-visual-clear-env.py +120 -0
  286. package/tools/test-owned-visual-clear.py +373 -0
  287. package/tools/verify-draft-runtime-gate.mjs +138 -0
  288. package/tools/verify-owned-runtime-release.mjs +114 -0
  289. 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,15 +113,21 @@ 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: 'Sign in with Carthagent Ship',
124
+ form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Ship credential instead of signing in?',
125
+ description: 'Leave disabled for fresh browser approval and updated account permissions.', default: false }] satisfies Form.Fields,
126
+ },
127
+ authorize: async (answer: Form.Answer) => {
128
+ // The legacy file intentionally survives migration. Never let its presence
129
+ // short-circuit an explicit sign-in or recovery from revoked/old scopes.
130
+ const legacy = answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
124
131
  if (legacy) {
125
132
  return {
126
133
  mode: 'auto' as const,
@@ -146,15 +153,21 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
146
153
  };
147
154
  },
148
155
  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
- }),
156
+ `${coordinationFile}:${credential.methodID}:${credential.refresh}`,
157
+ async () => {
158
+ const credentialID = await readNativeOAuthCredential({ credentialFile,
159
+ integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID, refreshToken: credential.refresh });
160
+ return refreshNativeCredential({
161
+ credential,
162
+ credentialFile: coordinationFile,
163
+ credentialID,
164
+ reread: () => readNativeOAuthCredential({ credentialFile, credentialID,
165
+ integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID }),
166
+ refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
167
+ ...oauthCredentials(await client.refresh({ refreshToken: current.refresh, signal: AbortSignal.timeout(10_000) }), now()),
168
+ ...(current.metadata ? { metadata: current.metadata } : {}) }),
169
+ });
170
+ },
158
171
  ),
159
172
  label: () => 'Carthagent Ship',
160
173
  };
@@ -164,6 +177,7 @@ function integrationDefinitions(editor: Parameters<Context['integration']['trans
164
177
  editor.method.update(oauthMethod(options));
165
178
  editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Use an Experiential API key' } });
166
179
  editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
180
+ editor.update(SHIP_INTEGRATION_ID, integration => { integration.name = EXPERIENTIAL_PROVIDER_NAME; });
167
181
  }
168
182
 
169
183
  function asModelInfo(providerID: string, model: { id: string }, baseURL: string) {
@@ -199,6 +213,7 @@ interface ProviderRegistrationOptions {
199
213
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
200
214
  agentDir?: string;
201
215
  credentialFile?: string;
216
+ vaultFile?: string;
202
217
  openBrowser?: typeof openBrowser;
203
218
  telemetryIdentity?: ReturnType<typeof createSessionIdentity>;
204
219
  sessionIdentity?: (event: SessionModelRequest) => string;
@@ -215,12 +230,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
215
230
  const agentDir = options.agentDir ?? shipAgentDir(env);
216
231
  const openBrowserImpl = options.openBrowser ?? openBrowser;
217
232
  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
- };
233
+ // credentialFile historically selects the coordination namespace. Retain
234
+ // the fixture shorthand (one explicit path for both) unless vaultFile is set.
235
+ const coordinationFile = options.credentialFile ?? nativeCredentialFile(env);
236
+ const credentialFile = options.vaultFile ?? options.credentialFile ?? nativeCredentialDatabaseFile(env, ctx.app.channel);
224
237
  const requestIdentity = options.sessionIdentity;
225
238
  const identity = options.telemetryIdentity ?? createSessionIdentity();
226
239
  const requestSecret = options.requestSecret ?? `${ctx.location?.project?.id || ctx.location?.directory || 'ctg'}:request-v1`;
@@ -236,23 +249,36 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
236
249
  return failures;
237
250
  };
238
251
  try {
252
+ const excludedIntegrations = new Set<string>();
239
253
  const integration = await ctx.integration.transform(editor => {
240
254
  integrationDefinitions(editor, {
241
255
  env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow: deviceAuth.start,
242
- rereadCredential,
243
- credentialFile: options.credentialFile ?? nativeCredentialFile(env),
256
+ credentialFile,
257
+ coordinationFile,
244
258
  });
259
+ for (const integration of editor.list()) {
260
+ const supported = nativeLoginProvider(integration.id);
261
+ if (supported) editor.update(integration.id, item => { item.name = supported.name; });
262
+ else {
263
+ excludedIntegrations.add(integration.id);
264
+ editor.remove(integration.id);
265
+ }
266
+ }
245
267
  });
246
268
  registrations.push(integration);
247
269
 
248
270
  const baseURL = experientialBaseUrl(env);
249
- const shipModels: Model.Info[] = [];
271
+ let shipModels: Model.Info[] = [];
272
+ let shipConnection: ConnectionInfo | undefined;
250
273
  const subscriptions: Record<string, Model.Info[]> = { [ANTHROPIC_PROVIDER_ID]: [], [OPENAI_CODEX_PROVIDER_ID]: [] };
251
274
  const subscriptionConnections: Partial<Record<string, ConnectionInfo>> = {};
252
275
  const providerRegistration = await ctx.provider.transform(editor => {
276
+ for (const { provider } of editor.list()) {
277
+ if (provider.id.startsWith('opencode') || excludedIntegrations.has(provider.integrationID ?? provider.id)) editor.remove(provider.id);
278
+ }
253
279
  editor.add({ info: providerInfo(EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
254
280
  '@opencode/ai/providers/openai-compatible', SHIP_INTEGRATION_ID,
255
- { baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels });
281
+ { baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels, sourceConnection: shipConnection });
256
282
  // Subscription IDs already exist in the native catalog. Overlay only the
257
283
  // definitions and retain catalog cost/capability metadata.
258
284
  for (const [providerID, baseURL] of [
@@ -330,8 +356,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
330
356
  const refresh = async (signal?: AbortSignal): Promise<void> => {
331
357
  const inventory = await loadShipModels({ ctx, fetchImpl, env, baseURL, signal });
332
358
  if (inventory?.models) {
333
- shipModels.splice(0, shipModels.length, ...inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
334
- inventory.managed ? cloudGatewayUrl(env) : baseURL)));
359
+ shipConnection = inventory.connection;
360
+ // The host indexes immutable model arrays by identity.
361
+ shipModels = inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
362
+ inventory.managed ? cloudGatewayUrl(env) : baseURL));
335
363
  await ctx.provider.reload();
336
364
  }
337
365
  for (const providerID of [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]) {
@@ -340,7 +368,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
340
368
  if (!active) {
341
369
  if (subscriptionConnections[providerID]) {
342
370
  delete subscriptionConnections[providerID];
343
- subscriptions[providerID].splice(0, subscriptions[providerID].length);
371
+ subscriptions[providerID] = [];
344
372
  await ctx.provider.reload();
345
373
  }
346
374
  continue;
@@ -357,7 +385,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
357
385
  const accountMetadata = credential && 'metadata' in credential ? credential.metadata as Record<string, unknown> | undefined : undefined;
358
386
  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
387
  if (prior?.account !== accountKey) {
360
- subscriptions[providerID].splice(0, subscriptions[providerID].length);
388
+ subscriptions[providerID] = [];
361
389
  }
362
390
  const known = new Map([
363
391
  ...catalogModels(providerID).map((model: CatalogModel) => [model.id, model] as const),
@@ -369,7 +397,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
369
397
  ? anthropicModelDefinition(model, base) : codexModelDefinition(model, base),
370
398
  providerID === ANTHROPIC_PROVIDER_ID ? 'https://api.anthropic.com' : 'https://chatgpt.com/backend-api');
371
399
  });
372
- subscriptions[providerID].splice(0, subscriptions[providerID].length, ...converted);
400
+ subscriptions[providerID] = converted;
373
401
  subscriptionConnections[providerID] = active;
374
402
  await ctx.storage.set(cacheKey, { account: accountKey, models: discovered, checkedAt: now() });
375
403
  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 = "File edits and commands use the direct read/write/edit/bash tools. A code-mode search() catalog lists only namespaced tools.* helpers, so their absence there does not mean they are unavailable. If a tool is locked, its error names the required delivery step; take that step instead of searching for alternatives."
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,69 @@
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' | '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 Ship 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 Ship 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 Ship 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('Ship 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 Ship OAuth connection is required.', {}); }
45
+ const action = (input as { action: 'checkout' | 'portal' }).action;
46
+ if (action !== 'checkout' && action !== 'portal') throw new Error('Unsupported Ship billing action.');
47
+ const returnUrl = `${options.cloudUrl ?? cloudControlUrl()}/account`;
48
+ const result = action === 'portal'
49
+ ? await cloud.portal({ accessToken: token, returnUrl, signal: call.signal })
50
+ : await cloud.checkout({ accessToken: token, successUrl: returnUrl, cancelUrl: returnUrl, signal: call.signal });
51
+ return { url: result.url };
52
+ },
53
+ shipLogout: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipLogout>) => {
54
+ call.signal.throwIfAborted();
55
+ let active: ActiveCredential;
56
+ let token: string;
57
+ try { ({ active, token } = await access()); }
58
+ catch { return call.error('unauthenticated', 'An active Carthagent Ship OAuth connection is required.', {}); }
59
+ const session = (input as { session?: string }).session;
60
+ if (session) {
61
+ await cloud.revokeSession({ accessToken: token, sessionId: session, signal: call.signal });
62
+ return { revoked: true };
63
+ }
64
+ await cloud.revokeCurrent({ accessToken: token, signal: call.signal });
65
+ await ctx.integration.transform((editor) => editor.remove(active.id));
66
+ return { revoked: true, credentialID: active.id };
67
+ },
68
+ };
69
+ }