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
package/bin/ctg.mjs CHANGED
@@ -1,736 +1,18 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Carthagent CLI - Evidence-driven delivery & contract verification
4
- */
5
- import { fileURLToPath } from 'node:url';
6
- import { basename, dirname, join, resolve } from 'node:path';
7
- import { readFileSync } from 'node:fs';
8
- import { spawn } from 'node:child_process';
9
- import { planningView } from '../lib/planning-view.mjs';
10
- import { startImplementation } from '../lib/planning.mjs';
11
- import { randomUUID } from 'node:crypto';
12
- import os from 'node:os';
13
- import {
14
- fingerprint,
15
- evidenceIdentity,
16
- evidenceFresh,
17
- completionIssues,
18
- validatePlan,
19
- planD2,
20
- runCommand
21
- } from '../lib/delivery.mjs';
22
- import { lockWorkspace, selectReport } from '../lib/workspace.mjs';
23
- import { beginReview, parseFindings } from '../lib/review-state.mjs';
24
- import { latestReport, saveReport } from '../lib/reports.mjs';
25
- import { normalizeReviewMode, resolveReviewMode, loadCarthagentConfig, saveCarthagentConfig, carthagentConfigPath, reviewerPrompt, parseVerdict, resolveStickyDefaults } from '../lib/review.mjs';
26
- import { compareVersions, fetchLatestRelease, detectInstallMethod, cleanStaleGlobalInstall } from '../lib/update.mjs';
27
- import { parseUpdateOptions, readUpdateChannel, writeUpdateChannel } from '../lib/update-channel.mjs';
28
- import { acceptanceReviewerPrompt } from '../lib/acceptance-review.mjs';
29
-
30
- // Ensure Carthagent operates completely isolated in its own agent directory (~/.carthagent/agent)
31
- // so it NEVER touches, reads, or piggybacks on any existing ~/.pi/agent installation.
32
- const defaultAgentDir = process.env.CARTHAGENT_AGENT_DIR || process.env.CARTHAGENT_CODING_AGENT_DIR || join(os.homedir(), '.carthagent', 'agent');
33
- if (!process.env.CARTHAGENT_CODING_AGENT_DIR && !process.env.CARTHAGENT_AGENT_DIR) {
34
- process.env.CARTHAGENT_CODING_AGENT_DIR = defaultAgentDir;
35
- }
36
- if (!process.env.PI_CODING_AGENT_DIR) {
37
- process.env.PI_CODING_AGENT_DIR = process.env.CARTHAGENT_CODING_AGENT_DIR || defaultAgentDir;
38
- }
39
-
40
- const __filename = fileURLToPath(import.meta.url);
41
- const __dirname = dirname(__filename);
42
- const cwd = process.cwd();
43
- const root = resolve(__dirname, '..');
44
- const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
2
+ import { readFile } from 'node:fs/promises';
3
+ import { runOpenCodeCli } from '../lib/opencode/cli.mjs';
4
+ import { createOpenCodeCliRuntime } from '../lib/opencode/cli-runtime.mjs';
45
5
 
46
6
  const argv = process.argv.slice(2);
47
- const command = argv[0];
48
-
49
- function printHelp() {
50
- console.log(`
51
- \x1b[1m\x1b[36mCarthagent\x1b[0m \x1b[90m${version}\x1b[0m \x1b[2m(ctg)\x1b[0m
52
-
53
- \x1b[1mUSAGE:\x1b[0m
54
- ctg Launch the interactive split-terminal delivery console
55
- ctg <task> Open the console and immediately deliver <task>
56
- ctg --list-models [search] List available provider models
57
- ctg <command> [options]
58
- ctg review workspace --request-file <task.txt> [--provider <name> --model <id>]
59
-
60
- \x1b[1mCOMMANDS:\x1b[0m
61
- \x1b[32mtui\x1b[0m, \x1b[32mui\x1b[0m Interactive console: live run feed + live plan.d2 side panel
62
- \x1b[32mresume\x1b[0m, \x1b[32msessions\x1b[0m Open the console with the session picker to resume prior work
63
- \x1b[32mcontinue\x1b[0m Open the console on the most recent session
64
- \x1b[32mdemo\x1b[0m Same console driven by a scripted mock run — no provider needed
65
- \x1b[32mstatus\x1b[0m, \x1b[32ms\x1b[0m Display workspace fingerprint, active contract & pending checks
66
- \x1b[32mcheck\x1b[0m [id], \x1b[32mc\x1b[0m [id] Execute check suite (default: 'all') in bounded subprocess
67
- \x1b[32mhash\x1b[0m, \x1b[32mfingerprint\x1b[0m Calculate workspace SHA-256 source freshness fingerprint
68
- \x1b[32mvalidate\x1b[0m <file.json> Validate acceptance contract against delivery schema
69
- \x1b[32mlogin\x1b[0m, \x1b[32mauth\x1b[0m Connect an AI provider (interactive login)
70
- \x1b[32maccount\x1b[0m Show Ship credit, plan, grants, and CLI sessions
71
- \x1b[32mbilling\x1b[0m [checkout|portal] Open Ship subscription billing
72
- \x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named Ship CLI session
73
- \x1b[32mserve\x1b[0m, \x1b[32mweb\x1b[0m, \x1b[32mstart\x1b[0m Start the interactive web dashboard on port 3000
74
- \x1b[32mtest\x1b[0m Run core unit test suite
75
- \x1b[32meval\x1b[0m [args...] Run evaluation harness (evaluate.mjs)
76
- \x1b[32mreview\x1b[0m Show self-review default · \x1b[32mreview\x1b[0m ask|yes|no sets it
77
- \x1b[32mreview\x1b[0m <pr> Run a fresh-context review of a pull request
78
- \x1b[32mupdate\x1b[0m [--check] [--channel stable|beta|experimental] Update or switch channels
79
- \x1b[32mhelp\x1b[0m, \x1b[32m--help\x1b[0m, \x1b[32m-h\x1b[0m Display this help message
80
-
81
- \x1b[1mTUI OPTIONS:\x1b[0m
82
- --provider <name> Provider for the AI agent (default: provider settings)
83
- --model <id> Model id or pattern (e.g. openrouter/inkling, sonnet)
84
- In the TUI, type on the model row to search/autocomplete
85
- --thinking <level> off|minimal|low|medium|high|xhigh|max
86
- --theme <name> opencode | tokyonight | nebula | ember | forest | mono |
87
- obsidian | midnight | nord | paper | daylight |
88
- solarized-dark | solarized-light | solarized |
89
- okabe-dark | okabe-light | okabe |
90
- contrast-dark | contrast-light | contrast | system
91
- --agent-cli, --engine-cli Explicit path to agent entrypoint
92
- --session <path|id> Resume a specific session file or partial session id
93
- --continue, -c Resume the most recent session for this directory
94
- --resume, -r Open the session picker at startup (TUI only)
95
- --validators <file> User-owned required-validator manifest (delivery)
96
- --context <file> Extra task/delivery context file
97
- --bash-cap <sec> Cap un-timed shell tool timeouts during a run
98
- --max-tools <n> Session tool-call budget (0 disables)
99
- --max-seconds <n> Session elapsed-time budget (0 disables)
100
- --max-repairs <n> Session automatic-repair budget (0 disables)
101
- --review <mode> Self-review loop after major changes: ask|yes|no
102
- (default: ~/.carthagent/config.json, else ask — /review in-session)
103
- --isolate Run with only Carthagent's delivery extension (no other extensions/skills)
104
- --no-strict Don't require delivery_plan before write/edit tools
105
- --no-delivery Run the console without the delivery extension
106
- --no-guide Send prompts verbatim instead of auto-applying delivery framing
107
- --guide Force delivery framing on (default)
108
- --ascii Plain-ASCII glyphs (for fonts that render icons as ?)
109
- --glyphs <mode> Glyph tier: auto (default) | unicode | ascii
110
- --codegraph-max-files <n> Maximum source files indexed for structural navigation (default 8000)
111
- --workflow <mode> standard (default) or strict tested-design ceremony
112
- --demo Drive the console with a scripted mock run (no provider)
113
- -p, --print Headless passthrough: text output without TUI
114
- --json Headless JSONL engine events (tool starts/results and messages)
115
- Also supported by ctg status --json for report snapshots
116
-
117
- \x1b[1mTUI KEYS:\x1b[0m
118
- enter send/steer · esc abort (again = force-restart agent) · ^r resume session · tab focus · shift+tab view
119
- up/down history/scroll · pgup/pgdn/wheel scroll focused pane · drag-select copies · ^t settings · ^n new session · x expand · ^c quit
120
-
121
- \x1b[1mEXAMPLES:\x1b[0m
122
- ctg
123
- ctg "Build a task CLI with tests"
124
- ctg --model sonnet --theme opencode "Fix the failing tests in src/"
125
- ctg --validators validators.json "Migrate the schema"
126
- `);
127
- }
128
-
129
- // ---------------------------------------------------------------- TUI routing
130
-
131
- import { validateBudgetOptions } from '../lib/budget.mjs';
132
-
133
- const TUI_FLAGS = {
134
- '--provider': 'provider', '--model': 'model', '--thinking': 'thinking', '--theme': 'theme',
135
- '--engine-cli': 'engineCli', '--agent-cli': 'engineCli', '--validators': 'validators', '--context': 'context', '--bash-cap': 'bashCap',
136
- '--max-tools': 'maxTools', '--max-seconds': 'maxSeconds', '--max-repairs': 'maxRepairs',
137
- '--session': 'session', '--review': 'review', '--glyphs': 'glyphs',
138
- '--codegraph-max-files': 'codegraphMaxFiles',
139
- '--workflow': 'workflow',
140
- };
141
- const TUI_BOOL = {
142
- '--isolate': ['isolate', true], '--no-strict': ['strict', false], '--strict': ['strict', true],
143
- '--no-delivery': ['delivery', false], '-p': ['print', true], '--print': ['print', true],
144
- '--no-guide': ['autoFraming', false], '--guide': ['autoFraming', true],
145
- '--ascii': ['ascii', true], '--json': ['json', true],
146
- '--demo': ['demo', true],
147
- '-c': ['continue', true], '--continue': ['continue', true],
148
- '-r': ['resume', true], '--resume': ['resume', true],
149
- };
150
-
151
- function parseTuiArgs(list) {
152
- const opts = {};
153
- const prompt = [];
154
- for (let i = 0; i < list.length; i++) {
155
- const a = list[i];
156
- if (a === '--') { prompt.push(...list.slice(i + 1)); break; }
157
- if (TUI_FLAGS[a]) {
158
- const value = list[++i];
159
- if (value === undefined || value.startsWith('--')) throw Error(`Missing value for ${a}`);
160
- opts[TUI_FLAGS[a]] = value; continue;
161
- }
162
- if (TUI_BOOL[a]) { const [k, v] = TUI_BOOL[a]; opts[k] = v; continue; }
163
- if (a.startsWith('-')) { console.error(`\x1b[31mUnknown flag:\x1b[0m ${a}`); process.exit(1); }
164
- prompt.push(a);
165
- }
166
- validateBudgetOptions(opts);
167
- if (prompt.length) opts.prompt = prompt.join(' ');
168
- return opts;
169
- }
170
-
171
- async function handleListModels(searchPattern) {
172
- const { loadAuthRuntime } = await import('../lib/tui/auth.mjs');
173
- const { modelMatchesQuery } = await import('../lib/tui/models.mjs');
174
- const { providerDisplayName } = await import('../lib/providers/names.mjs');
175
- const runtime = await loadAuthRuntime();
176
- const models = runtime.getAvailableSnapshot()
177
- .filter(model => !searchPattern || modelMatchesQuery({ ...model, full: `${model.provider}/${model.id}`, provider: providerDisplayName(model.provider) }, searchPattern))
178
- .sort((a, b) => providerDisplayName(a.provider).localeCompare(providerDisplayName(b.provider)) || a.id.localeCompare(b.id));
179
- for (const model of models) {
180
- const fields = [providerDisplayName(model.provider), model.id, model.contextWindow, model.maxTokens, model.input, model.output];
181
- console.log(fields.map(value => value == null ? '?' : value).join(' '));
182
- }
183
- }
184
-
185
- async function handleTui(list) {
186
- const opts = parseTuiArgs(list);
187
- // Sticky theme/model: explicit --theme/--model flags win, else fall back to
188
- // the last choice persisted in ~/.carthagent/config.json. Undefined lets the engine
189
- // apply its own default rather than forcing a stale id.
190
- const sticky = resolveStickyDefaults(opts);
191
- opts.theme = sticky.theme;
192
- opts.model = sticky.model;
193
- if (opts.demo && !process.stdout.isTTY) { console.error('demo requires an interactive terminal'); process.exit(1); }
194
- // Headless passthrough: explicit --print, or stdout isn't a TTY
195
- if (opts.print || opts.json || !process.stdout.isTTY) {
196
- // The session picker needs a TTY — headless -r continues the most recent.
197
- if (opts.resume) { opts.resume = false; opts.continue = true; }
198
- const { locateEngine, buildEngineArgs } = await import('../lib/tui/app.mjs');
199
- const { shouldFrame, framePrompt } = await import('../lib/tui/framing.mjs');
200
- let engineCmd;
201
- try { engineCmd = locateEngine(opts.engineCli); } catch (e) { console.error(e.message); process.exit(1); }
202
- if (!opts.prompt) { console.error('non-interactive mode requires a prompt'); process.exit(1); }
203
- const { hasConfiguredProvider, loadAuthRuntime } = await import('../lib/tui/auth.mjs');
204
- if (!hasConfiguredProvider()) {
205
- console.error('No AI provider connected. Run `ctg login` to connect one (Carthagent Ship is recommended), or set an API key env var (e.g. ANTHROPIC_API_KEY).');
206
- process.exit(1);
207
- }
208
- // Refresh the dynamic Cloud catalog before the engine child reads its
209
- // persisted model store. This keeps headless/RPC discovery in sync with
210
- // browser login without fabricating model metadata.
211
- await loadAuthRuntime();
212
- const framingOn = (opts.autoFraming ?? true);
213
- const prompt = shouldFrame(opts.prompt, { enabled: framingOn, delivery: opts.delivery !== false })
214
- ? framePrompt(opts.prompt) : opts.prompt;
215
- const args = [...engineCmd.args, '-p', ...buildEngineArgs({ isolate: true, ...opts }), '--', prompt];
216
- const child = spawn(engineCmd.cmd, args, {
217
- stdio: 'inherit',
218
- cwd: process.cwd(),
219
- env: {
220
- ...process.env,
221
- CARTHAGENT_CODING_AGENT_DIR: process.env.CARTHAGENT_CODING_AGENT_DIR || defaultAgentDir,
222
- PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR || defaultAgentDir,
223
- CARTHAGENT_RUN_MODE: 'print'
224
- }
225
- });
226
- child.on('error', error => { console.error(error.message); process.exitCode = 1; });
227
- child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
228
- return;
229
- }
230
- const { runTui } = await import('../lib/tui/app.mjs');
231
- await runTui(opts);
232
- }
233
-
234
- async function handleStatus() {
235
- const currentFp = fingerprint(cwd, ['.']);
236
- const report = latestReport(cwd);
237
- if (argv.includes('--json')) {
238
- const state = report?.state;
239
- console.log(JSON.stringify({ version: 1, workspace: cwd, currentFingerprint: currentFp, report: report?.path || null, state: state || null, planningView: planningView(state, currentFp), completionIssues: state?.plan ? completionIssues(state, cwd, currentFp) : null }));
240
- return;
241
- }
242
- console.log(`\x1b[1mWorkspace:\x1b[0m ${cwd}`);
243
- console.log(`\x1b[1mFingerprint:\x1b[0m \x1b[36m${currentFp}\x1b[0m`);
244
- if (!report?.state?.plan) {
245
- console.log('\x1b[90mNo delivery report found for this workspace.\x1b[0m');
246
- return;
247
- }
248
- const { state } = report;
249
- const checkStatus = check => {
250
- const evidence = state.evidence?.[check.id];
251
- if (!evidence) return 'pending';
252
- if (!evidence.passed) return 'failed';
253
- return evidenceFresh(check, evidence, currentFp, state) ? 'passed' : 'stale';
254
- };
255
- console.log(`\x1b[1mStatus:\x1b[0m ${state.status}`);
256
- console.log(`\x1b[1mGoal:\x1b[0m ${state.plan.goal}`);
257
- console.log(`\x1b[1mChecks:\x1b[0m ${state.plan.checks.map(check => `${checkStatus(check)}:${check.id}`).join(', ')}`);
258
- console.log(`\x1b[1mReport:\x1b[0m ${report.path}`);
259
- }
260
-
261
- async function handleHash() {
262
- const roots = argv.slice(1).length > 0 ? argv.slice(1) : ['.'];
263
- try {
264
- const hash = fingerprint(cwd, roots);
265
- console.log(hash);
266
- } catch (err) {
267
- console.error(`\x1b[31mError calculating fingerprint:\x1b[0m ${err.message}`);
268
- process.exit(1);
269
- }
270
- }
271
-
272
- async function handleCheck() {
273
- const release = lockWorkspace(cwd);
274
- try { await runWorkspaceChecks(); } finally { release(); }
275
- }
276
-
277
- async function runWorkspaceChecks() {
278
- const checkId = argv[1] || 'all';
279
- const report = latestReport(cwd);
280
- if (!report?.state?.plan) {
281
- console.error('No delivery report found for this workspace.');
282
- process.exitCode = 1;
283
- return;
284
- }
285
- selectReport(cwd, report.path);
286
- const state = report.state;
287
- const checks = checkId === 'all' ? state.plan.checks : state.plan.checks.filter(check => check.id === checkId);
288
- if (!checks.length) {
289
- console.error(`Unknown check ID: ${checkId}`);
290
- process.exitCode = 1;
291
- return;
292
- }
293
-
294
- startImplementation(state, fingerprint(cwd, ['.']));
295
- let allPassed = true;
296
- const priorStatus = state.status;
297
- state.status = 'verifying';
298
- state.verificationStarted = true;
299
- saveReport(report.path, state);
300
- for (const check of checks) {
301
- const before = fingerprint(cwd, ['.']);
302
- console.log(`\n\x1b[1m${check.id}\x1b[0m ${check.argv.join(' ')}`);
303
- const result = await runCommand(check.argv, {
304
- cwd,
305
- timeoutSeconds: check.timeoutSeconds,
306
- logPath: join(report.dir, `${check.id}-${randomUUID()}.log`)
307
- });
308
- const after = fingerprint(cwd, ['.']);
309
- const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after;
310
- state.evidence[check.id] = {
311
- passed,
312
- fingerprint: after,
313
- ...evidenceIdentity(state, check),
314
- executedRevision: state.revision,
315
- code: result.code,
316
- timedOut: result.timedOut,
317
- cancelled: result.cancelled,
318
- outputLimit: result.outputLimit,
319
- changedDuringCheck: before !== after,
320
- durationMs: result.durationMs,
321
- logPath: result.logPath,
322
- outputTail: result.output.slice(-1200)
323
- };
324
- saveReport(report.path, state);
325
- if (result.output) console.log(result.output.trim());
326
- console.log(passed ? `\x1b[32mpassed\x1b[0m (${result.durationMs}ms)` : `\x1b[31mfailed\x1b[0m (exit ${result.code})`);
327
- allPassed &&= passed;
328
- }
329
- const complete = Object.values(completionIssues(state, cwd, fingerprint(cwd, ['.']))).every(items => items.length === 0);
330
- state.status = allPassed && complete && priorStatus === 'verified' ? 'verified' : 'implementing';
331
- saveReport(report.path, state);
332
- if (!allPassed) process.exitCode = 1;
333
- }
334
-
335
- async function handleValidate() {
336
- const filePath = argv[1];
337
- if (!filePath) {
338
- console.error('Usage: ctg validate <path-to-plan.json>');
339
- process.exit(1);
340
- }
341
- try {
342
- const raw = readFileSync(resolve(cwd, filePath), 'utf8');
343
- const plan = JSON.parse(raw);
344
- const validated = validatePlan(plan, cwd);
345
- console.log('\x1b[32m✓ Acceptance contract is VALID.\x1b[0m');
346
- console.log(`Goal: ${validated.goal}`);
347
- console.log(`Checks: ${validated.checks.map(c => c.id).join(', ')}`);
348
- const d2 = planD2(validated);
349
- console.log('\n\x1b[1mGenerated D2 Outline:\x1b[0m\n' + d2);
350
- } catch (err) {
351
- console.error('\x1b[31m✕ Validation Error:\x1b[0m', err.message);
352
- process.exit(1);
353
- }
354
- }
355
-
356
- async function handleLogin() {
357
- if (!process.stdout.isTTY) {
358
- console.error('login requires an interactive terminal — or set an API key env var (see README)');
359
- process.exit(1);
360
- }
361
- // Use the same Carthagent provider picker as in-session /login and /auth.
362
- // This keeps ordering, recommendation metadata, and credential storage from
363
- // drifting between shell and TUI entry points.
364
- const { runTui } = await import('../lib/tui/app.mjs');
365
- await runTui({ autoFraming: false, openAuth: true, loginOnly: true });
366
- }
367
-
368
- async function cloudRuntime() {
369
- const { loadAuthRuntime } = await import('../lib/tui/auth.mjs');
370
- return loadAuthRuntime();
371
- }
372
-
373
- async function cloudFailure(error) {
374
- if (error.message === 'cloud_login_required') console.error('Carthagent Ship is not signed in. Run `ctg login` and choose Carthagent Ship.');
375
- else {
376
- const { cloudManagedUsageRecovery } = await import('../lib/cloud/client.mjs');
377
- console.error(cloudManagedUsageRecovery(error) || `Carthagent Ship: ${error.message}`);
378
- }
379
- process.exitCode = 1;
380
- }
381
-
382
- async function handleAccount() {
383
- try {
384
- const { formatCloudAccount, loadCloudAccount } = await import('../lib/tui/auth.mjs');
385
- const account = await loadCloudAccount(await cloudRuntime());
386
- console.log(formatCloudAccount(account));
387
- const grants = Array.isArray(account.grants) ? account.grants : [];
388
- if (grants.length) {
389
- const { formatNanoUsd } = await import('../lib/cloud/client.mjs');
390
- console.log('\nCredit grants:');
391
- for (const grant of grants) console.log(` ${formatNanoUsd(grant.remainingNanoUsd)} remaining of ${formatNanoUsd(grant.originalNanoUsd)} · ${grant.source}${grant.expiresAt ? ` · expires ${grant.expiresAt}` : ''}`);
392
- }
393
- const sessions = Array.isArray(account.sessions) ? account.sessions : [];
394
- if (sessions.length) {
395
- console.log('\nCLI sessions:');
396
- for (const session of sessions) console.log(` ${session.id} · ${session.revoked_at ? 'revoked' : 'active'} · ${session.client_name || 'Carthagent CLI'} · last used ${session.last_used_at || session.created_at}`);
397
- }
398
- } catch (error) { await cloudFailure(error); }
399
- }
400
-
401
- async function handleBilling() {
402
- const action = argv[1] || 'checkout';
403
- if (!['checkout', 'portal'].includes(action)) { console.error('Usage: ctg billing [checkout|portal]'); process.exitCode = 1; return; }
404
- try {
405
- const { createCloudBillingLink } = await import('../lib/tui/auth.mjs');
406
- const { openBrowser } = await import('../lib/cloud/client.mjs');
407
- const result = await createCloudBillingLink(await cloudRuntime(), action);
408
- const opened = await openBrowser(result.url).catch(() => false);
409
- console.log(`${opened ? 'Opened' : 'Open'} ${result.url}`);
410
- } catch (error) { await cloudFailure(error); }
411
- }
412
-
413
- async function handleCloudLogout() {
414
- try {
415
- const runtime = await cloudRuntime();
416
- const sessionId = argv[1];
417
- if (sessionId) {
418
- const { revokeCloudSession } = await import('../lib/tui/auth.mjs');
419
- await revokeCloudSession(runtime, sessionId);
420
- console.log(`Revoked Ship session ${sessionId}.`);
421
- } else {
422
- const { CloudClient, cloudControlUrl } = await import('../lib/cloud/client.mjs');
423
- const { CARTHAGENT_SHIP_PROVIDER_ID } = await import('../lib/providers/names.mjs');
424
- if ((await runtime.checkAuth?.(CARTHAGENT_SHIP_PROVIDER_ID))?.type !== 'oauth') throw new Error('cloud_login_required');
425
- const accessToken = (await runtime.getAuth(CARTHAGENT_SHIP_PROVIDER_ID))?.auth?.apiKey;
426
- if (!accessToken) throw new Error('cloud_login_required');
427
- await new CloudClient({ baseUrl: cloudControlUrl() }).revokeCurrent({ accessToken });
428
- await runtime.logout(CARTHAGENT_SHIP_PROVIDER_ID);
429
- console.log('Revoked and removed the current Carthagent Ship session.');
430
- }
431
- } catch (error) { await cloudFailure(error); }
432
- }
433
-
434
- function handleServe() {
435
- console.log('\x1b[1m[Carthagent]\x1b[0m Launching Evidence-Driven Delivery server...');
436
- const serverScript = join(root, 'server.mjs');
437
- const child = spawn(process.execPath, [serverScript], {
438
- stdio: 'inherit',
439
- cwd: root,
440
- env: {
441
- ...process.env,
442
- CARTHAGENT_WORKSPACE: cwd,
443
- CARTHAGENT_CODING_AGENT_DIR: process.env.CARTHAGENT_CODING_AGENT_DIR || defaultAgentDir,
444
- PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR || defaultAgentDir
445
- }
446
- });
447
- child.on('exit', (code) => process.exit(code || 0));
448
- }
449
-
450
- function handleTest() {
451
- console.log('\x1b[1m[Carthagent]\x1b[0m Running unit test suite...');
452
- const child = spawn(process.execPath, ['--test', 'tests/*.test.mjs'], {
453
- stdio: 'inherit',
454
- cwd: root,
455
- env: process.env
456
- });
457
- child.on('exit', (code) => process.exit(code || 0));
458
- }
459
-
460
- /**
461
- * `ctg review` — self-review control surface:
462
- * ctg review show the effective default and where it's set
463
- * ctg review ask|yes|no persist the default to ~/.carthagent/config.json
464
- * ctg review <pr> run a detached fresh-context reviewer over a PR
465
- */
466
- async function handleReview(args) {
467
- if (args[0] === 'workspace') {
468
- let requestFile, provider, model, thinking = 'high';
469
- for (let i = 1; i < args.length; i++) {
470
- const flag = args[i], value = args[++i];
471
- if (!value) throw Error(`Missing value for ${flag}`);
472
- if (flag === '--request-file') requestFile = value;
473
- else if (flag === '--provider') provider = value;
474
- else if (flag === '--model') model = value;
475
- else if (flag === '--thinking') thinking = value;
476
- else throw Error(`Unknown workspace review option: ${flag}`);
477
- }
478
- if (!requestFile) throw Error('Use ctg review workspace --request-file <original-task.txt> [--provider name --model id]');
479
- return handleTui(['--print', '--no-delivery', '--no-guide', '--isolate', '--review', 'no', '--thinking', thinking, ...(provider ? ['--provider', provider] : []), ...(model ? ['--model', model] : []), '--', acceptanceReviewerPrompt(readFileSync(resolve(requestFile), 'utf8'))]);
480
- }
481
- const ref = args[0];
482
- if (!ref || ref === 'status') {
483
- const config = loadCarthagentConfig();
484
- console.log(`\x1b[1mself-review default:\x1b[0m \x1b[36m${resolveReviewMode(undefined, config)}\x1b[0m (config: ${config.review ?? 'unset'} @ ${carthagentConfigPath()})`);
485
- console.log('\nusage:');
486
- console.log(' ctg review ask|yes|no Set the default for all sessions');
487
- console.log(' ctg review <pr> Fresh-context review of a pull request now');
488
- console.log(' ctg --review <mode> Per-launch override · /review inside a session');
489
- return;
490
- }
491
- const mode = normalizeReviewMode(ref);
492
- if (mode) {
493
- saveCarthagentConfig({ review: mode });
494
- console.log(`\x1b[32mself-review default → ${mode}\x1b[0m (saved to ${carthagentConfigPath()})`);
495
- return;
496
- }
497
- if (ref.startsWith('-')) {
498
- console.error(`\x1b[31mUnknown review option:\x1b[0m ${ref}`);
499
- process.exit(2);
500
- }
501
- await runReview(ref, args.slice(1));
502
- }
503
-
504
- /**
505
- * Fresh-context review of a pull request: validates the PR via `gh`, then
506
- * spawns the bundled engine headless with no extensions/skills so the
507
- * reviewer brings no shared context. Exit 0 = APPROVE, 1 = CHANGES-REQUESTED,
508
- * 2 = infrastructure failure (gh, engine, timeout).
509
- */
510
- async function runReview(prRef, rest) {
511
- let timeoutSeconds = 300, engineCli;
512
- for (let i = 0; i < rest.length; i++) {
513
- if (rest[i] === '--timeout') {
514
- timeoutSeconds = Number(rest[++i]);
515
- if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 300) { console.error('Review timeout must be an integer from 1 to 300'); process.exit(2); }
516
- }
517
- else if (rest[i] === '--agent-cli' || rest[i] === '--engine-cli') engineCli = rest[++i];
518
- else { console.error(`\x1b[31mUnknown option:\x1b[0m ${rest[i]}`); process.exit(2); }
519
- }
520
- const view = await runCommand(['gh', 'pr', 'view', prRef, '--json', 'title,url,headRefName,baseRefName,headRefOid'], { cwd, timeoutSeconds: 30 });
521
- if (view.code !== 0) {
522
- console.error(`\x1b[31mCannot load PR ${prRef}\x1b[0m — is gh installed and authenticated, and is ${cwd} a GitHub repo clone?`);
523
- if (view.output.trim()) console.error(view.output.trim());
524
- process.exit(2);
525
- }
526
- let meta = {};
527
- try { meta = JSON.parse(view.output); } catch { }
528
- if (!/^[a-f0-9]{40,64}$/i.test(meta.headRefOid || '')) { console.error('PR head identity unavailable'); process.exit(2); }
529
- const { locateEngine } = await import('../lib/engine.mjs');
530
- let engineCmd;
531
- try { engineCmd = locateEngine(engineCli); } catch (e) { console.error(e.message); process.exit(2); }
532
- const snapshot = fingerprint(cwd, ['.']);
533
- let round;
534
- try { round = beginReview(cwd, meta.url || String(prRef), snapshot, meta.headRefOid); }
535
- catch (error) { console.error(error.message); process.exitCode = 2; return; }
536
- try {
537
- const prompt = reviewerPrompt({ prRef, cwd, meta }) + `\nImmediately before the final VERDICT line emit one single-line REVIEW_JSON: object with version:1, snapshot:${JSON.stringify(snapshot)}, head:${JSON.stringify(meta.headRefOid)}, findings:[{file,line,severity,issue,fix}]. Severity must be blocking or nonblocking; line is a positive integer. Approval requires no blocking findings. No markdown fences.`;
538
- const logPath = join(cwd, '.harness', 'reviews', `review-${Date.now()}.log`);
539
- const result = await runCommand(
540
- [engineCmd.cmd, ...engineCmd.args, '-p', '--no-extensions', '--no-skills', '--no-prompt-templates', '--', prompt],
541
- { cwd, timeoutSeconds, logPath }
542
- );
543
- if (result.output.trim()) process.stdout.write(result.output.trim() + '\n');
544
- console.log(`\x1b[90mreview log: ${logPath}\x1b[0m`);
545
- if (result.timedOut) throw Error(`reviewer timed out after ${timeoutSeconds}s`);
546
- if (result.cancelled || result.code !== 0) throw Error(`reviewer exited abnormally (code ${result.code})`);
547
- const after = await runCommand(['gh', 'pr', 'view', prRef, '--json', 'title,url,headRefName,baseRefName,headRefOid'], { cwd, timeoutSeconds: 30 });
548
- let afterMeta;
549
- try { afterMeta = JSON.parse(after.output); } catch { }
550
- if (after.code !== 0 || afterMeta?.headRefOid !== meta.headRefOid) throw Error('PR head changed or unavailable; verdict is stale');
551
- if (fingerprint(cwd, ['.']) !== snapshot) throw Error('Review source snapshot changed; verdict is stale');
552
- const verdict = parseVerdict(result.output);
553
- if (!verdict) throw Error('reviewer produced no VERDICT line — treat as inconclusive');
554
- const findings = parseFindings(result.output, snapshot, meta.headRefOid, verdict);
555
- round.finish(verdict === 'APPROVE' ? 'approved' : 'changes-requested', findings);
556
- process.exitCode = verdict === 'APPROVE' ? 0 : 1;
557
- } catch (error) {
558
- round.finish('inconclusive', { error: error.message });
559
- console.error(error.message);
560
- process.exitCode = 2;
561
- } finally { round.release(); }
562
- }
563
-
564
- /**
565
- * `ctg update [--check] [--force]` — resolve the latest release (control plane
566
- * first, GitHub releases fallback), then re-install it the same way it was
567
- * originally installed: checkout the tag in a clone install, or re-run the
568
- * tagged `github:` npm install otherwise.
569
- */
570
- async function handleUpdate(args) {
571
- let options, channel, latest;
572
- try {
573
- options = parseUpdateOptions(args);
574
- channel = options.channel ?? readUpdateChannel();
575
- latest = (await fetchLatestRelease({ channel })).release;
576
- } catch (error) { console.error(error.message); process.exitCode = 2; return; }
577
- const { checkOnly, force } = options;
578
- const remember = () => { if (options.channel !== null) writeUpdateChannel(channel); };
579
- const target = `ctg update${options.channel === null ? '' : ` --channel ${channel}`}`;
580
- console.log(`Update channel: ${channel}${channel === 'stable' ? '' : ' — prerelease software may be unstable'}`);
581
- // npm is a .cmd shim on Windows and cannot spawn with shell:false — run it via cmd.exe.
582
- const npmStep = npmArgs => process.platform === 'win32' ? ['cmd.exe', '/d', '/s', '/c', 'npm', ...npmArgs] : ['npm', ...npmArgs];
583
- const runStep = async step => {
584
- const result = await runCommand(step.argv, { cwd: root, timeoutSeconds: step.timeout ?? 600 });
585
- if (result.code !== 0) {
586
- console.error(`\x1b[31m${step.label} failed${result.timedOut ? ' (timed out)' : ''}:\x1b[0m`);
587
- if (result.output.trim()) console.error(result.output.trim());
588
- process.exit(1);
589
- }
590
- };
591
- // Global installs die with ENOTDIR/EEXIST when a stray `carthagent` entry or a
592
- // `.carthagent-*` stash leftover occupies the global node_modules — clear and retry once.
593
- const npmInstallStep = async step => {
594
- const run = () => runCommand(step.argv, { cwd: root, timeoutSeconds: step.timeout ?? 600 });
595
- let result = await run();
596
- if (result.code !== 0 && /\bENOTDIR\b|\bEEXIST\b|\bENOTEMPTY\b/.test(result.output)) {
597
- const probe = await runCommand(npmStep(['root', '-g']), { cwd: root, timeoutSeconds: 30 });
598
- const removed = probe.code === 0 ? cleanStaleGlobalInstall(probe.output.trim()) : [];
599
- if (removed.length) {
600
- console.log(`\x1b[33mRemoved stale global install ${removed.length === 1 ? 'entry' : 'entries'} (${removed.map(p => basename(p)).join(', ')}) — retrying.\x1b[0m`);
601
- result = await run();
602
- }
603
- }
604
- if (result.code !== 0) {
605
- console.error(`\x1b[31m${step.label} failed${result.timedOut ? ' (timed out)' : ''}:\x1b[0m`);
606
- if (result.output.trim()) console.error(result.output.trim());
607
- process.exit(1);
608
- }
609
- };
610
- if (latest.minNode && compareVersions(process.versions.node, latest.minNode) < 0) {
611
- console.error(`\x1b[31mCarthagent ${latest.version} requires Node ≥ ${latest.minNode}\x1b[0m — this runtime is ${process.versions.node}`);
612
- process.exit(1);
613
- }
614
- const delta = compareVersions(version, latest.version);
615
- if (delta === 0 && !force) { if (!checkOnly) remember(); console.log(`\x1b[32mCarthagent ${version} is already the latest release.\x1b[0m`); return; }
616
- if (delta > 0 && !force && options.channel === null) { console.log(`\x1b[33mInstalled ${version} is ahead of latest release ${latest.version} — pass --force to reinstall anyway.\x1b[0m`); return; }
617
- if (checkOnly) {
618
- console.log(`\x1b[33mCarthagent ${latest.version} is available${latest.note ? ` — ${latest.note}` : ''}\x1b[0m (installed: ${version}) — run \x1b[36m${target}\x1b[0m`);
619
- return;
620
- }
621
- const method = detectInstallMethod(root);
622
- console.log(`\x1b[36mUpdating Carthagent ${version} → ${latest.version}\x1b[0m (${method} install)`);
623
- if (method === 'clone') {
624
- const dirty = await runCommand(['git', 'status', '--porcelain'], { cwd: root, timeoutSeconds: 30 });
625
- if (dirty.code !== 0) { console.error(`\x1b[31mCannot inspect the clone at ${root}\x1b[0m`); process.exit(1); }
626
- if (dirty.output.trim()) { console.error(`\x1b[31mThe clone at ${root} has uncommitted changes.\x1b[0m Commit, stash, or update it manually: git fetch --tags && git checkout ${latest.tag} && npm ci && npm install -g .`); process.exit(1); }
627
- await runStep({ argv: ['git', 'fetch', 'origin', '--tags', '--force'], label: 'git fetch --tags' });
628
- await runStep({ argv: ['git', 'checkout', '--detach', latest.tag], label: `git checkout ${latest.tag}` });
629
- await runStep({ argv: npmStep(['ci']), label: 'npm ci', timeout: 600 });
630
- await npmInstallStep({ argv: npmStep(['install', '-g', '.']), label: 'npm install -g .', timeout: 600 });
631
- } else {
632
- await npmInstallStep({ argv: npmStep(['install', '-g', '--install-links=true', latest.installSpec]), label: `npm install ${latest.installSpec}`, timeout: 600 });
633
- }
634
- remember();
635
- console.log(`\x1b[32mCarthagent updated to ${latest.version}.\x1b[0m Restart any running console to pick it up.`);
636
- }
637
-
638
- function handleEval() {
639
- const evalScript = join(root, 'evaluate.mjs');
640
- const child = spawn(process.execPath, [evalScript, ...argv.slice(1)], {
641
- stdio: 'inherit',
642
- cwd: root,
643
- env: process.env
644
- });
645
- child.on('exit', (code) => process.exit(code || 0));
646
- }
647
-
648
- // Router
649
- switch (command) {
650
- case 'tui':
651
- case 'ui':
652
- case 'interactive':
653
- handleTui(argv.slice(1));
654
- break;
655
- case 'demo':
656
- handleTui([...argv.slice(1), '--demo']);
657
- break;
658
- case 'resume':
659
- case 'sessions':
660
- handleTui([...argv.slice(1), '--resume']);
661
- break;
662
- case 'continue':
663
- handleTui([...argv.slice(1), '--continue']);
664
- break;
665
- case 'status':
666
- case 's':
667
- handleStatus();
668
- break;
669
- case 'hash':
670
- case 'fingerprint':
671
- handleHash();
672
- break;
673
- case 'check':
674
- case 'c':
675
- handleCheck();
676
- break;
677
- case 'validate':
678
- handleValidate();
679
- break;
680
- case 'login':
681
- case 'auth':
682
- handleLogin();
683
- break;
684
- case 'account':
685
- case 'cloud':
686
- handleAccount();
687
- break;
688
- case 'billing':
689
- handleBilling();
690
- break;
691
- case 'logout-cloud':
692
- handleCloudLogout();
693
- break;
694
- case 'serve':
695
- case 'web':
696
- case 'start':
697
- handleServe();
698
- break;
699
- case 'test':
700
- handleTest();
701
- break;
702
- case 'eval':
703
- handleEval();
704
- break;
705
- case 'review':
706
- handleReview(argv.slice(1));
707
- break;
708
- case 'update':
709
- handleUpdate(argv.slice(1));
710
- break;
711
- case 'help':
712
- case '--help':
713
- case '-h':
714
- printHelp();
715
- break;
716
- case '--version':
717
- case '-v':
718
- console.log(version);
719
- break;
720
- case '--list-models':
721
- handleListModels(argv.slice(1).join(' '));
722
- break;
723
- case undefined:
724
- // Bare `ctg` → interactive split-terminal console
725
- handleTui([]);
726
- break;
727
- default:
728
- // engine-style: unknown first arg means the args ARE the task prompt
729
- // e.g. `ctg "Build a task CLI"` or `ctg --model sonnet "Fix tests"`
730
- if (command.startsWith('-') && !TUI_FLAGS[command] && !TUI_BOOL[command]) {
731
- console.error(`\x1b[31mUnknown command or flag:\x1b[0m ${command}`);
732
- printHelp();
733
- process.exit(1);
734
- }
735
- handleTui(argv);
7
+ if (argv[0] === 'native') argv.shift();
8
+ process.env.CTG_NODE_EXECUTABLE ??= process.execPath;
9
+
10
+ try {
11
+ const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
12
+ const runtime = await createOpenCodeCliRuntime();
13
+ const code = await runOpenCodeCli({ argv, runtime, version, stdoutIsTTY: Boolean(process.stdout.isTTY) });
14
+ process.exitCode = Number.isInteger(code) ? code : 0;
15
+ } catch (error) {
16
+ console.error(error.message);
17
+ process.exitCode = Number.isInteger(error.exitCode) ? error.exitCode : 1;
736
18
  }