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.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +11 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +129 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +451 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode-facing command-line adapter.
|
|
3
|
+
*
|
|
4
|
+
* The CLI deliberately depends on a small runtime port rather than importing
|
|
5
|
+
* OpenCode implementation modules. The native service owns process startup,
|
|
6
|
+
* authentication, model discovery, and session execution; this module owns
|
|
7
|
+
* stable ctg argv compatibility and output/exit-code behavior.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const VALUE_FLAGS = new Map([
|
|
11
|
+
['--provider', 'provider'], ['--model', 'model'], ['--thinking', 'thinking'],
|
|
12
|
+
['--theme', 'theme'], ['--engine-cli', 'engineCli'], ['--agent-cli', 'engineCli'],
|
|
13
|
+
['--session', 'session'], ['--validators', 'validators'], ['--context', 'context'],
|
|
14
|
+
['--bash-cap', 'bashCap'], ['--max-tools', 'maxTools'], ['--max-seconds', 'maxSeconds'],
|
|
15
|
+
['--max-repairs', 'maxRepairs'], ['--review', 'review'], ['--glyphs', 'glyphs'],
|
|
16
|
+
['--codegraph-max-files', 'codegraphMaxFiles'], ['--workflow', 'workflow'],
|
|
17
|
+
['--rewrite-cap', 'rewriteCap'], ['--tool-output-cap', 'toolOutputCap'], ['--turn-delay-ms', 'turnDelayMs'],
|
|
18
|
+
['--jev-provider', 'jevProvider'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const BOOL_FLAGS = new Map([
|
|
22
|
+
['--isolate', ['isolate', true]], ['--strict', ['strict', true]], ['--no-strict', ['strict', false]],
|
|
23
|
+
['--no-delivery', ['delivery', false]], ['--delivery', ['delivery', true]],
|
|
24
|
+
['--no-guide', ['autoFraming', false]], ['--guide', ['autoFraming', true]],
|
|
25
|
+
['--ascii', ['ascii', true]], ['--json', ['json', true]], ['-p', ['print', true]],
|
|
26
|
+
['--protect-existing', ['protectExisting', true]], ['--jev', ['jev', true]], ['--no-jev', ['jev', false]],
|
|
27
|
+
['--print', ['print', true]], ['--demo', ['demo', true]], ['-c', ['continue', true]],
|
|
28
|
+
['--continue', ['continue', true]], ['-r', ['resume', true]], ['--resume', ['resume', true]],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const aliases = new Map([
|
|
32
|
+
['ui', 'tui'], ['interactive', 'tui'], ['sessions', 'resume'], ['s', 'status'],
|
|
33
|
+
['fingerprint', 'hash'], ['c', 'check'], ['auth', 'login'], ['cloud', 'account'], ['upgrade', 'update'],
|
|
34
|
+
['archives', 'archive'],
|
|
35
|
+
['web', 'serve'], ['start', 'serve'],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export function parseOpenCodeArgs(argv) {
|
|
39
|
+
const options = {};
|
|
40
|
+
const prompt = [];
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const arg = argv[i];
|
|
43
|
+
if (arg === '--') {
|
|
44
|
+
prompt.push(...argv.slice(i + 1));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
48
|
+
const value = argv[++i];
|
|
49
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
50
|
+
options[VALUE_FLAGS.get(arg)] = value;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (BOOL_FLAGS.has(arg)) {
|
|
54
|
+
const [key, value] = BOOL_FLAGS.get(arg);
|
|
55
|
+
options[key] = value;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg.startsWith('-')) throw new Error(`Unknown flag: ${arg}`);
|
|
59
|
+
prompt.push(arg);
|
|
60
|
+
}
|
|
61
|
+
if (prompt.length) options.prompt = prompt.join(' ');
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function cliHelp(version = '') {
|
|
66
|
+
return `\n\x1b[1m\x1b[36mCarthagent\x1b[0m${version ? ` \x1b[90m${version}\x1b[0m` : ''} \x1b[2m(ctg)\x1b[0m
|
|
67
|
+
|
|
68
|
+
\x1b[1mUSAGE:\x1b[0m
|
|
69
|
+
ctg Launch the native Carthagent delivery console
|
|
70
|
+
ctg <task> Start a session and deliver <task>
|
|
71
|
+
ctg --list-models [search] List available provider models
|
|
72
|
+
ctg <command> [options]
|
|
73
|
+
ctg service stop Stop this profile’s background service safely
|
|
74
|
+
ctg review workspace --request-file <task.txt> [--provider <name> --model <id>]
|
|
75
|
+
|
|
76
|
+
\x1b[1mCOMMANDS:\x1b[0m
|
|
77
|
+
native [args] Compatibility alias; native is now the default
|
|
78
|
+
\x1b[32mtui\x1b[0m, \x1b[32mui\x1b[0m Interactive console
|
|
79
|
+
\x1b[32mresume\x1b[0m, \x1b[32msessions\x1b[0m Open the session picker
|
|
80
|
+
\x1b[32mcontinue\x1b[0m Continue the most recent session
|
|
81
|
+
\x1b[32mdemo\x1b[0m Run the scripted demonstration
|
|
82
|
+
\x1b[32mstatus\x1b[0m, \x1b[32ms\x1b[0m Display workspace fingerprint, contract and checks
|
|
83
|
+
\x1b[32mcheck\x1b[0m [id], \x1b[32mc\x1b[0m [id] Execute a declared check
|
|
84
|
+
\x1b[32mhash\x1b[0m, \x1b[32mfingerprint\x1b[0m Calculate workspace SHA-256 fingerprint
|
|
85
|
+
\x1b[32mvalidate\x1b[0m <file.json> Validate acceptance contract
|
|
86
|
+
\x1b[32mlogin\x1b[0m, \x1b[32mauth\x1b[0m Connect an AI provider
|
|
87
|
+
\x1b[32maccount\x1b[0m Show Ship credit, plan, grants and CLI sessions
|
|
88
|
+
\x1b[32mbilling\x1b[0m [checkout|portal] Open Ship billing
|
|
89
|
+
\x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named Ship session
|
|
90
|
+
\x1b[32marchive\x1b[0m [list|search|read|export] Read-only legacy session archive
|
|
91
|
+
\x1b[32mreview\x1b[0m Configure self-review or review a PR
|
|
92
|
+
\x1b[32mserve\x1b[0m, \x1b[32mweb\x1b[0m, \x1b[32mstart\x1b[0m Start the web dashboard
|
|
93
|
+
\x1b[32mtest\x1b[0m Run core tests
|
|
94
|
+
\x1b[32meval\x1b[0m [args...] Run evaluation harness
|
|
95
|
+
\x1b[32mupdate\x1b[0m [--check] [--channel stable|beta|experimental] Update Carthagent
|
|
96
|
+
\x1b[32mhelp\x1b[0m, \x1b[32m--help\x1b[0m, \x1b[32m-h\x1b[0m Display this help
|
|
97
|
+
|
|
98
|
+
\x1b[1mOPTIONS:\x1b[0m
|
|
99
|
+
--provider <name> Provider for the agent
|
|
100
|
+
--model <id> Model id or pattern
|
|
101
|
+
--thinking <level> off|minimal|low|medium|high|xhigh|max
|
|
102
|
+
--theme <name> Carthagent theme name
|
|
103
|
+
--engine-cli, --agent-cli <path> Explicit native runtime executable override
|
|
104
|
+
--session <path|id> Resume a specific session
|
|
105
|
+
--continue, -c Resume the most recent session
|
|
106
|
+
--resume, -r Open the session picker
|
|
107
|
+
--validators <file> Required-validator manifest
|
|
108
|
+
--context <file> Extra task context file
|
|
109
|
+
--bash-cap <sec> Cap shell tool timeouts
|
|
110
|
+
--max-tools <n> Session tool-call budget (0 disables)
|
|
111
|
+
--max-seconds <n> Session elapsed-time budget (0 disables)
|
|
112
|
+
--max-repairs <n> Automatic-repair budget (0 disables)
|
|
113
|
+
--protect-existing Preserve files present at session start
|
|
114
|
+
--rewrite-cap <n> Whole-file writes per path (0 disables)
|
|
115
|
+
--tool-output-cap <n> Bound text output per tool result
|
|
116
|
+
--turn-delay-ms <n> Pace tool loops
|
|
117
|
+
--jev, --no-jev Enable/disable advisory plan analysis (on for Ship models)
|
|
118
|
+
--jev-provider <name> ship|openrouter|typesafe
|
|
119
|
+
--review <mode> Self-review: ask|yes|no
|
|
120
|
+
--isolate Isolate extension/skill discovery (not a worktree)
|
|
121
|
+
--no-strict Disable delivery-plan write gate
|
|
122
|
+
--no-delivery Run without delivery behavior
|
|
123
|
+
--no-guide Disable prompt framing
|
|
124
|
+
-p, --print Headless text output
|
|
125
|
+
--json Headless JSONL events
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const commandNames = new Set([
|
|
130
|
+
'tui', 'resume', 'continue', 'demo', 'status', 'hash', 'check', 'validate', 'login',
|
|
131
|
+
'account', 'billing', 'logout-cloud', 'archive', 'serve', 'test', 'eval', 'review', 'update', 'quit', 'q', 'exit',
|
|
132
|
+
'logout', 'service',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
function fail(message, code = 1, io = console) {
|
|
136
|
+
io.error(message);
|
|
137
|
+
return code;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ensureMethod(runtime, method) {
|
|
141
|
+
if (typeof runtime?.[method] !== 'function') {
|
|
142
|
+
throw new Error(`Native Carthagent CLI runtime does not provide ${method}()`);
|
|
143
|
+
}
|
|
144
|
+
return runtime[method].bind(runtime);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute a CLI invocation using the native runtime port.
|
|
149
|
+
*
|
|
150
|
+
* Runtime contract (all operations are real operations, not optional no-ops):
|
|
151
|
+
* run({mode, options, prompt, args}), listModels({query}), login(), account(),
|
|
152
|
+
* billing({action}), logout({session}), review({args}), domain({command,args}),
|
|
153
|
+
* and help/version metadata are supplied by the caller. Interactive vs
|
|
154
|
+
* headless selection is made here so all aliases share identical behavior.
|
|
155
|
+
*/
|
|
156
|
+
export async function runOpenCodeCli({ argv = [], runtime, io = console, stdoutIsTTY = Boolean(process.stdout.isTTY), version = '' } = {}) {
|
|
157
|
+
const first = argv[0];
|
|
158
|
+
const normalized = aliases.get(first) || first;
|
|
159
|
+
const rest = commandNames.has(normalized) ? argv.slice(1) : (first === '--list-models' ? [] : argv);
|
|
160
|
+
|
|
161
|
+
if (['help', '--help', '-h'].includes(first)) {
|
|
162
|
+
io.log(cliHelp(version));
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
if (['--version', '-v'].includes(first)) {
|
|
166
|
+
io.log(version);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
if (first === '--list-models') {
|
|
170
|
+
try { await ensureMethod(runtime, 'listModels')({ query: argv.slice(1).join(' ') }); return 0; }
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
|
|
173
|
+
return fail(error.message, 1, io);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (['status', 'hash', 'fingerprint', 'check', 'c', 'validate'].includes(normalized)) {
|
|
177
|
+
try { return Number((await ensureMethod(runtime, 'domain')({ command: normalized, args: rest })) ?? 0); }
|
|
178
|
+
catch (error) { return fail(error.message, 1, io); }
|
|
179
|
+
}
|
|
180
|
+
if (normalized === 'review') {
|
|
181
|
+
try { return Number((await ensureMethod(runtime, 'review')({ args: rest, cwd: process.cwd() })) ?? 0); }
|
|
182
|
+
catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
|
|
183
|
+
}
|
|
184
|
+
if (normalized === 'login' || normalized === 'auth') {
|
|
185
|
+
if (!stdoutIsTTY) return fail('login requires an interactive terminal — or set a provider API key environment variable', 1, io);
|
|
186
|
+
try { return Number((await ensureMethod(runtime, 'login')({ args: rest })) ?? 0); }
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
|
|
189
|
+
return fail(error.message, 1, io);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (normalized === 'logout') return fail('Use `logout-cloud` to revoke a Carthagent Ship session; provider credentials are managed by the native runtime.', 1, io);
|
|
193
|
+
if (normalized === 'account' || normalized === 'cloud') {
|
|
194
|
+
try { return Number((await ensureMethod(runtime, 'account')({ args: rest })) ?? 0); }
|
|
195
|
+
catch (error) { return fail(error.message, 1, io); }
|
|
196
|
+
}
|
|
197
|
+
if (normalized === 'billing') {
|
|
198
|
+
const action = rest[0] || 'checkout';
|
|
199
|
+
if (!['checkout', 'portal'].includes(action)) return fail('Usage: ctg billing [checkout|portal]', 1, io);
|
|
200
|
+
try { return Number((await ensureMethod(runtime, 'billing')({ action })) ?? 0); }
|
|
201
|
+
catch (error) { return fail(error.message, 1, io); }
|
|
202
|
+
}
|
|
203
|
+
if (normalized === 'logout-cloud') {
|
|
204
|
+
try { return Number((await ensureMethod(runtime, 'logout')({ session: rest[0] })) ?? 0); }
|
|
205
|
+
catch (error) { return fail(error.message, 1, io); }
|
|
206
|
+
}
|
|
207
|
+
if (normalized === 'archive') {
|
|
208
|
+
try { return Number((await ensureMethod(runtime, 'archive')({ args: rest })) ?? 0); }
|
|
209
|
+
catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
|
|
210
|
+
}
|
|
211
|
+
if (['serve', 'service', 'test', 'eval', 'update', 'quit', 'q', 'exit', 'demo'].includes(normalized)) {
|
|
212
|
+
try { return Number((await ensureMethod(runtime, 'command')({ command: normalized, args: rest })) ?? 0); }
|
|
213
|
+
catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let mode = normalized;
|
|
217
|
+
let args = rest;
|
|
218
|
+
if (first === undefined) { mode = 'tui'; args = []; }
|
|
219
|
+
else if (!commandNames.has(normalized)) {
|
|
220
|
+
if (first.startsWith('-') && !VALUE_FLAGS.has(first) && !BOOL_FLAGS.has(first)) {
|
|
221
|
+
io.error(`Unknown command or flag: ${first}`);
|
|
222
|
+
io.log(cliHelp(version));
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
mode = 'tui';
|
|
226
|
+
}
|
|
227
|
+
if (mode === 'resume') mode = 'tui';
|
|
228
|
+
if (mode === 'continue') mode = 'tui';
|
|
229
|
+
if (mode === 'demo') mode = 'tui';
|
|
230
|
+
|
|
231
|
+
let options;
|
|
232
|
+
try { options = parseOpenCodeArgs(args); }
|
|
233
|
+
catch (error) { return fail(error.message, 1, io); }
|
|
234
|
+
if (normalized === 'resume' || normalized === 'continue' || normalized === 'demo') {
|
|
235
|
+
options[normalized === 'resume' ? 'resume' : normalized === 'continue' ? 'continue' : 'demo'] = true;
|
|
236
|
+
}
|
|
237
|
+
if (options.demo && !stdoutIsTTY) return fail('demo requires an interactive terminal', 1, io);
|
|
238
|
+
const headless = options.print || options.json || !stdoutIsTTY;
|
|
239
|
+
if (headless && options.resume) { options.resume = false; options.continue = true; }
|
|
240
|
+
if (headless && !options.prompt) return fail('non-interactive mode requires a prompt', 1, io);
|
|
241
|
+
try { return Number((await ensureMethod(runtime, 'run')({ mode: headless ? 'headless' : 'interactive', options, prompt: options.prompt, args })) ?? 0); }
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
|
|
244
|
+
return fail(error.message, Number.isInteger(error?.exitCode) ? error.exitCode : 1, io);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const openCodeCliManifest = Object.freeze({
|
|
249
|
+
aliases: Object.freeze(Object.fromEntries(aliases)),
|
|
250
|
+
valueFlags: Object.freeze(Object.fromEntries(VALUE_FLAGS)),
|
|
251
|
+
boolFlags: Object.freeze(Object.fromEntries([...BOOL_FLAGS].map(([flag, [key, value]]) => [flag, { key, value }]))),
|
|
252
|
+
});
|
|
@@ -8,6 +8,8 @@ import os from 'node:os';
|
|
|
8
8
|
import { shipAgentDir } from '../ship-auth.mjs';
|
|
9
9
|
import { CARTHAGENT_SHIP_PROVIDER_ID } from '../providers/names.mjs';
|
|
10
10
|
import { withFileLock } from './file-lock.mjs';
|
|
11
|
+
import { Credential } from '@opencode/schema/credential';
|
|
12
|
+
import { Schema } from 'effect';
|
|
11
13
|
|
|
12
14
|
export const CTG_CREDENTIAL_NAMESPACE = 'ctg.credentials/';
|
|
13
15
|
const refreshFlights = new Map();
|
|
@@ -45,12 +47,60 @@ export function nativeCredentialFile(env = process.env, platform = process.platf
|
|
|
45
47
|
return join(dataHome, 'opencode', 'auth.json');
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/** OpenCode 2.0.32 packages/cli/src/database-path.ts and util/global-roots.ts.
|
|
51
|
+
* Unlike legacy auth.json, the native credential vault is in the channel DB.
|
|
52
|
+
* The running host supplies its channel; never infer it from a release label. */
|
|
53
|
+
export function nativeCredentialDatabaseFile(env = process.env, channel, home = os.homedir()) {
|
|
54
|
+
if (!channel) throw new Error('The native runtime channel is required to locate its credential vault.');
|
|
55
|
+
const data = join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'opencode');
|
|
56
|
+
const filename = env.OPENCODE_DB ?? (['latest', 'dev', 'beta', 'next', 'prod'].includes(channel)
|
|
57
|
+
|| ['1', 'true'].includes(env.OPENCODE_DISABLE_CHANNEL_DB)
|
|
58
|
+
? 'opencode.db' : `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, '-')}.db`);
|
|
59
|
+
if (filename === ':memory:') throw new Error('Native OAuth refresh requires a file-backed credential vault.');
|
|
60
|
+
return resolve(data, filename);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Raw read only: connection.resolve() refreshes expired OAuth and must never
|
|
64
|
+
* be called from its own refresh callback. Schema provenance: pinned 2.0.32
|
|
65
|
+
* packages/core/src/credential/sql.ts; value is Credential.Value JSON. Opening
|
|
66
|
+
* read-only must fail for missing/changed vaults, never create a parallel store.
|
|
67
|
+
* Bun is the native host; node:sqlite supports the same regression under Node.
|
|
68
|
+
* @param {{credentialFile: string, credentialID?: string, integrationID: string, methodID: string, refreshToken?: string}} options
|
|
69
|
+
*/
|
|
70
|
+
export async function readNativeOAuthCredential({ credentialFile, credentialID = undefined, integrationID, methodID, refreshToken = undefined }) {
|
|
71
|
+
const db = typeof globalThis.Bun !== 'undefined'
|
|
72
|
+
? new (await import('bun:sqlite')).Database(credentialFile, { readonly: true, strict: true })
|
|
73
|
+
: new (await import('node:sqlite')).DatabaseSync(credentialFile, { readOnly: true });
|
|
74
|
+
try {
|
|
75
|
+
// Capture the callback's original row, never the current active selection.
|
|
76
|
+
// LIMIT 2 detects duplicate grants without selecting an arbitrary account.
|
|
77
|
+
const query = credentialID
|
|
78
|
+
? 'SELECT id, value FROM credential WHERE id = ? AND integration_id = ?'
|
|
79
|
+
: `SELECT id, value FROM credential WHERE integration_id = ? AND json_extract(value, '$.type') = 'oauth'
|
|
80
|
+
AND json_extract(value, '$.methodID') = ? AND json_extract(value, '$.refresh') = ? LIMIT 2`;
|
|
81
|
+
const statement = typeof db.prepare === 'function' ? db.prepare(query) : db.query(query);
|
|
82
|
+
const rows = credentialID ? statement.all(credentialID, integrationID) : statement.all(integrationID, methodID, refreshToken);
|
|
83
|
+
if (rows.length > 1) throw new Error('Ambiguous native OAuth credential identity; no refresh was attempted.');
|
|
84
|
+
const row = rows[0];
|
|
85
|
+
if (!row) throw new Error('The native OAuth credential is no longer stored; reconnect before refreshing.');
|
|
86
|
+
// Do not include SQLite/JSON/schema errors or their input values in logs.
|
|
87
|
+
let value;
|
|
88
|
+
try { value = Schema.decodeUnknownSync(Credential.Value)(JSON.parse(row.value)); }
|
|
89
|
+
catch { throw new Error('Unsupported native credential vault value; no refresh was attempted.'); }
|
|
90
|
+
if (value.type !== 'oauth' || value.methodID !== methodID) {
|
|
91
|
+
throw new Error('The native OAuth credential method changed; no refresh was attempted.');
|
|
92
|
+
}
|
|
93
|
+
return credentialID ? value : row.id;
|
|
94
|
+
} finally { db.close(); }
|
|
95
|
+
}
|
|
96
|
+
|
|
48
97
|
/** Serialize refresh across host processes, rereading OpenCode's vault under the lock.
|
|
49
|
-
* @param {{credential: any, credentialFile: string, reread: () => Promise<any>, refresh: (credential: any) => Promise<any>, timeout?: number}} options
|
|
98
|
+
* @param {{credential: any, credentialFile: string, credentialID?: string, reread: () => Promise<any>, refresh: (credential: any) => Promise<any>, timeout?: number}} options
|
|
50
99
|
*/
|
|
51
100
|
export async function refreshNativeCredential({
|
|
52
101
|
credential,
|
|
53
102
|
credentialFile,
|
|
103
|
+
credentialID,
|
|
54
104
|
reread,
|
|
55
105
|
refresh,
|
|
56
106
|
timeout = 15_000,
|
|
@@ -63,6 +113,8 @@ export async function refreshNativeCredential({
|
|
|
63
113
|
// Token material is never written to the lease path or logs.
|
|
64
114
|
const digest = createHash('sha256').update(`${credential.methodID || ''}\0${credential.refresh}`).digest('hex').slice(0, 32);
|
|
65
115
|
const lock = `${profile}.ctg-refresh-${digest}.lock`;
|
|
116
|
+
// Keep the existing token-scoped lock path so old hosts cannot rotate in
|
|
117
|
+
// parallel. Old unbound receipts are uncertain and must not be adopted.
|
|
66
118
|
const pending = `${lock}.pending`;
|
|
67
119
|
const credentialDigest = value => createHash('sha256')
|
|
68
120
|
.update(`${value?.methodID || credential.methodID || ''}\0${value?.refresh || ''}`).digest('hex').slice(0, 32);
|
|
@@ -74,7 +126,6 @@ export async function refreshNativeCredential({
|
|
|
74
126
|
if (persistedReceipt === receipt?.raw) await rm(pending, { force: true });
|
|
75
127
|
return latest;
|
|
76
128
|
};
|
|
77
|
-
const current = await reread();
|
|
78
129
|
// OpenCode persists refresh callback results only after this callback
|
|
79
130
|
// resolves. Leave a token-free handoff receipt so another host that wins
|
|
80
131
|
// the disk lock during that gap waits for the native vault write instead
|
|
@@ -82,9 +133,13 @@ export async function refreshNativeCredential({
|
|
|
82
133
|
let receipt;
|
|
83
134
|
try {
|
|
84
135
|
receipt = JSON.parse(await readFile(pending, 'utf8'));
|
|
85
|
-
receipt.raw = JSON.stringify({ pid: receipt.pid, owner: receipt.owner, from: receipt.from, to: receipt.to, stage: receipt.stage, createdAt: receipt.createdAt });
|
|
136
|
+
receipt.raw = JSON.stringify({ pid: receipt.pid, owner: receipt.owner, from: receipt.from, to: receipt.to, stage: receipt.stage, createdAt: receipt.createdAt, credentialID: receipt.credentialID });
|
|
86
137
|
}
|
|
87
138
|
catch (error) { if (error.code !== 'ENOENT') throw new Error('Invalid pending native credential refresh receipt; inspect the CTG refresh receipt before retrying.'); }
|
|
139
|
+
if (credentialID && receipt && receipt.credentialID !== credentialID) {
|
|
140
|
+
throw new Error('Unbound or different native credential refresh receipt; no rotation was attempted. Reauthenticate or inspect the receipt before retrying.');
|
|
141
|
+
}
|
|
142
|
+
const current = await reread();
|
|
88
143
|
if (current?.type === 'oauth' && current.refresh && current.refresh !== credential.refresh) {
|
|
89
144
|
if (receipt?.from === digest && receipt.to === credentialDigest(current)) {
|
|
90
145
|
const persistedReceipt = await readFile(pending, 'utf8').catch(() => '');
|
|
@@ -108,22 +163,22 @@ export async function refreshNativeCredential({
|
|
|
108
163
|
// authoritative read before treating the outcome as uncertain.
|
|
109
164
|
const persisted = await acceptPersistedRotation();
|
|
110
165
|
if (persisted) return persisted;
|
|
111
|
-
throw new Error('The native refresh owner exited before
|
|
166
|
+
throw new Error('The native refresh owner exited before Carthagent runtime saved its credential. No second rotation was attempted; inspect the CTG refresh receipt.');
|
|
112
167
|
}
|
|
113
168
|
await new Promise(resolve => setTimeout(resolve, 40));
|
|
114
169
|
}
|
|
115
|
-
throw new Error('Timed out waiting for
|
|
170
|
+
throw new Error('Timed out waiting for Carthagent runtime to persist its refreshed credential; no second refresh was attempted.');
|
|
116
171
|
}
|
|
117
172
|
|
|
118
173
|
const owner = randomUUID();
|
|
119
|
-
const initialReceipt = JSON.stringify({ pid: process.pid, owner, from: digest, stage: 'refreshing', createdAt: Date.now() });
|
|
174
|
+
const initialReceipt = JSON.stringify({ pid: process.pid, owner, from: digest, stage: 'refreshing', createdAt: Date.now(), credentialID });
|
|
120
175
|
const pendingHandle = await open(pending, 'wx', 0o600);
|
|
121
176
|
try { await pendingHandle.writeFile(initialReceipt); await pendingHandle.sync(); }
|
|
122
177
|
finally { await pendingHandle.close(); }
|
|
123
178
|
const saveReceipt = async value => {
|
|
124
179
|
const temporary = `${pending}.${owner}.${value.stage}.tmp`;
|
|
125
180
|
const handle = await open(temporary, 'wx', 0o600);
|
|
126
|
-
try { await handle.writeFile(JSON.stringify(value)); await handle.sync(); }
|
|
181
|
+
try { await handle.writeFile(JSON.stringify({ ...value, credentialID })); await handle.sync(); }
|
|
127
182
|
finally { await handle.close(); }
|
|
128
183
|
if (await readFile(pending, 'utf8').catch(() => '') !== initialReceipt) {
|
|
129
184
|
await rm(temporary, { force: true });
|
|
@@ -201,5 +256,5 @@ export async function legacyOAuthForOpenCode(options = {}) {
|
|
|
201
256
|
}
|
|
202
257
|
|
|
203
258
|
export function legacyCredentialMigrationGuidance() {
|
|
204
|
-
return 'Existing Carthagent Ship OAuth can be migrated from the legacy auth.json credential into
|
|
259
|
+
return 'Existing Carthagent Ship OAuth can be migrated from the legacy auth.json credential into Carthagent runtime integration storage; other provider credentials remain BYOK and may be reconnected through Carthagent runtime if their format is unsupported.';
|
|
205
260
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { runCommand } from '../delivery.mjs';
|
|
5
|
+
|
|
6
|
+
/** Run an honest, isolated offline walkthrough; it never contacts a provider. */
|
|
7
|
+
/** @param {{ io?: Console, execute?: typeof runCommand, signal?: AbortSignal, nodeBinary?: string }} options */
|
|
8
|
+
export async function runOfflineDemo({ io = console, execute = runCommand, signal, nodeBinary = process.env.CTG_NODE_EXECUTABLE || process.execPath } = {}) {
|
|
9
|
+
signal?.throwIfAborted();
|
|
10
|
+
const workspace = await mkdtemp(join(tmpdir(), 'carthagent-native-demo-'));
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(join(workspace, 'test'));
|
|
13
|
+
await writeFile(join(workspace, 'app.mjs'), `export function summarize(items) {
|
|
14
|
+
return items.filter(item => item.done).map(item => item.title).join(', ');
|
|
15
|
+
}
|
|
16
|
+
`);
|
|
17
|
+
await writeFile(join(workspace, 'test', 'app.test.mjs'), `import test from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { summarize } from '../app.mjs';
|
|
20
|
+
|
|
21
|
+
test('summarize includes completed items and preserves their order', () => {
|
|
22
|
+
assert.equal(summarize([{ title: 'Plan', done: true }, { title: 'Ship', done: false }, { title: 'Test', done: true }]), 'Plan, Test');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('summarize handles an empty list', () => {
|
|
26
|
+
assert.equal(summarize([]), '');
|
|
27
|
+
});
|
|
28
|
+
`);
|
|
29
|
+
io.log('Carthagent native offline demo');
|
|
30
|
+
io.log('Scripted fixture only: no provider/API calls, no edits to your project, and no claim of project delivery evidence.');
|
|
31
|
+
io.log('\nPlan: implement a tiny completed-task summarizer, then verify ordering and the empty case.');
|
|
32
|
+
io.log(`\nFixture workspace: ${workspace}`);
|
|
33
|
+
const env = { ...process.env };
|
|
34
|
+
// Node's test runner marks subprocesses as nested workers; clear the marker
|
|
35
|
+
// so the fixture really runs when /demo is itself exercised from node:test.
|
|
36
|
+
delete env.NODE_TEST_CONTEXT;
|
|
37
|
+
const result = await execute([nodeBinary, '--test', 'test/app.test.mjs'], { cwd: workspace, timeoutSeconds: 30, signal, env });
|
|
38
|
+
if (result.output?.trim()) io.log(`\n${result.output.trim()}`);
|
|
39
|
+
if (result.cancelled || signal?.aborted) {
|
|
40
|
+
io.log('\nOffline demo cancelled; no completion is claimed.');
|
|
41
|
+
return 130;
|
|
42
|
+
}
|
|
43
|
+
if (result.code !== 0 || result.timedOut || result.cancelled) {
|
|
44
|
+
io.error(`Offline demo check failed${result.timedOut ? ' (timed out)' : ''}: ${result.output?.trim() || 'no test output'}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
io.log('\nVerification: isolated Node test suite passed (2 tests).');
|
|
48
|
+
io.log('Finish: the sample behavior is verified only inside this disposable demo fixture.');
|
|
49
|
+
return 0;
|
|
50
|
+
} finally {
|
|
51
|
+
await rm(workspace, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -84,6 +84,9 @@ export default Plugin.define({
|
|
|
84
84
|
await pendingWrites.get(event.sessionID);
|
|
85
85
|
const session = await ctx.session.get({ sessionID: event.sessionID });
|
|
86
86
|
const configured = session.metadata?.[budgetKey];
|
|
87
|
+
// This hook is an admission guard for evaluation sessions, not a
|
|
88
|
+
// general model-request gate for unrelated native host workflows.
|
|
89
|
+
if (configured === undefined) return;
|
|
87
90
|
if (!configured || typeof configured !== 'object'
|
|
88
91
|
|| configured.maxTurns !== maxTurns || configured.maxCost !== maxCost) {
|
|
89
92
|
throw Error('Evaluation session budget handshake is missing or differs from the active admission guard');
|
|
@@ -37,13 +37,14 @@ async function bounded(promise, milliseconds, fallback) {
|
|
|
37
37
|
|
|
38
38
|
/** Run one native OpenCode session with fail-closed accounting and cancellation. */
|
|
39
39
|
export async function runNativeEvaluation({ directory, prompt, systemPrompt = '', model, provider, timeoutSeconds = 600, maxTurns = 100, maxCost = 3,
|
|
40
|
-
signal, onEvent = () => {}, client, baseUrl = process.env.CTG_OPENCODE_URL, headers = {}, version = '2.0.32', outcomeTimeoutSeconds = 10
|
|
40
|
+
signal, onEvent = () => {}, client, baseUrl = process.env.CTG_OPENCODE_URL, headers = {}, version = '2.0.32', outcomeTimeoutSeconds = 10,
|
|
41
|
+
agent, metadata = {} }) {
|
|
41
42
|
positive(timeoutSeconds, 'timeoutSeconds', 3600);
|
|
42
43
|
positive(maxTurns, 'maxTurns', 1000);
|
|
43
44
|
positive(maxCost, 'maxCost');
|
|
44
45
|
positive(outcomeTimeoutSeconds, 'outcomeTimeoutSeconds', 60);
|
|
45
46
|
if (!directory || !prompt || !model || !provider) throw Error('directory, prompt, provider, and model are required');
|
|
46
|
-
if (!client && !baseUrl) throw Error('CTG_OPENCODE_URL must identify the isolated native
|
|
47
|
+
if (!client && !baseUrl) throw Error('CTG_OPENCODE_URL must identify the isolated native Carthagent runtime service; shared service discovery is disabled.');
|
|
47
48
|
if (!client && !Object.keys(headers).length && process.env.CTG_OPENCODE_AUTHORIZATION) headers = { authorization: process.env.CTG_OPENCODE_AUTHORIZATION };
|
|
48
49
|
const api = client || OpenCode.make({ baseUrl, headers });
|
|
49
50
|
const controller = new AbortController();
|
|
@@ -55,7 +56,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
55
56
|
const timer = setTimeout(() => controller.abort(new Error('Evaluation wall-time limit')), timeoutSeconds * 1000);
|
|
56
57
|
const started = Date.now();
|
|
57
58
|
const state = { turns: 0, productiveTurns: 0, providerErrors: 0, cost: null, tokens: 0, tools: {}, errors: [], lastStop: null, budgetReason: null, deliveryStatus: null };
|
|
58
|
-
let session, streamTask, streamError, outcome;
|
|
59
|
+
let session, streamTask, streamError, outcome, actualRuntimeVersion;
|
|
59
60
|
let interruptionStatus = 'not-needed';
|
|
60
61
|
let finished = false;
|
|
61
62
|
let stopAfterIdle = false;
|
|
@@ -76,7 +77,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
76
77
|
state.lastStop = data.finish;
|
|
77
78
|
} else if (event.type === 'session.execution.failed') {
|
|
78
79
|
state.providerErrors++;
|
|
79
|
-
state.errors.push(data.error?.message || '
|
|
80
|
+
state.errors.push(data.error?.message || 'Carthagent runtime execution failed');
|
|
80
81
|
} else if (event.type === 'session.tool.called') {
|
|
81
82
|
toolByCallID.set(data.id, data);
|
|
82
83
|
const name = data.tool || data.name;
|
|
@@ -106,7 +107,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
106
107
|
do {
|
|
107
108
|
const info=await api.session.get({sessionID:session.id},{signal:AbortSignal.timeout(5000)});
|
|
108
109
|
if(info.outcome) return info;
|
|
109
|
-
if(Date.now()>=deadline) throw Error('
|
|
110
|
+
if(Date.now()>=deadline) throw Error('Carthagent runtime session terminal outcome did not become available');
|
|
110
111
|
await new Promise(resolve=>setTimeout(resolve,25));
|
|
111
112
|
} while(true);
|
|
112
113
|
};
|
|
@@ -114,15 +115,20 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
114
115
|
try {
|
|
115
116
|
if (controller.signal.aborted) throw controller.signal.reason;
|
|
116
117
|
const info = await api.server.info({ signal: controller.signal });
|
|
117
|
-
|
|
118
|
+
actualRuntimeVersion = info.version;
|
|
119
|
+
const ownedRuntimeVersion = `0.0.0-ctg.${version}`;
|
|
120
|
+
if (actualRuntimeVersion !== version && actualRuntimeVersion !== ownedRuntimeVersion) {
|
|
121
|
+
throw Error(`Carthagent runtime compatible with ${version} required, received ${actualRuntimeVersion ?? 'unknown'}`);
|
|
122
|
+
}
|
|
118
123
|
const plugins = await api.plugin.list({ location: { directory } }, { signal: controller.signal });
|
|
119
124
|
const guard = plugins.data.find(plugin => plugin.id === EVALUATION_BUDGET_PLUGIN);
|
|
120
125
|
if (guard?.state.status !== 'active') throw Error(`Evaluation admission guard ${EVALUATION_BUDGET_PLUGIN} is not active`);
|
|
121
126
|
session = await api.session.create({
|
|
127
|
+
...(agent ? { agent } : {}),
|
|
122
128
|
model: { providerID: provider, id: model },
|
|
123
129
|
location: { directory },
|
|
124
130
|
title: `CTG evaluation ${NATIVE_EVALUATION_RUNTIME}`,
|
|
125
|
-
metadata: { [EVALUATION_BUDGET_METADATA]: { maxTurns, maxCost } },
|
|
131
|
+
metadata: { ...metadata, [EVALUATION_BUDGET_METADATA]: { maxTurns, maxCost } },
|
|
126
132
|
}, { signal: controller.signal });
|
|
127
133
|
streamTask = onEventStream();
|
|
128
134
|
// Subscription has no replay; allow its connection to establish before admission.
|
|
@@ -140,7 +146,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
140
146
|
// authoritative terminal outcome is visible; absence is never success.
|
|
141
147
|
const sessionInfo = await pollOutcome();
|
|
142
148
|
outcome = sessionInfo.outcome;
|
|
143
|
-
if (outcome !== 'succeeded') state.errors.push(`
|
|
149
|
+
if (outcome !== 'succeeded') state.errors.push(`Carthagent runtime session ended with authoritative outcome: ${outcome}`);
|
|
144
150
|
|
|
145
151
|
// Fetch only this session's assistant transcript; /session/stats is project-wide.
|
|
146
152
|
const transcript = await allMessages(api, session.id, { signal: AbortSignal.timeout(15000) });
|
|
@@ -159,7 +165,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
159
165
|
}
|
|
160
166
|
const tokenUsage = sessionInfo.tokens;
|
|
161
167
|
if (!Number.isFinite(sessionInfo.cost) || sessionInfo.cost < 0 || !tokenUsage || !['input','output','reasoning'].every(key => Number.isFinite(tokenUsage[key]) && tokenUsage[key] >= 0) || !tokenUsage.cache || !['read','write'].every(key => Number.isFinite(tokenUsage.cache[key]) && tokenUsage.cache[key] >= 0)) {
|
|
162
|
-
throw Error('
|
|
168
|
+
throw Error('Carthagent runtime session aggregate usage is unavailable; refusing evaluation without enforceable max-cost');
|
|
163
169
|
}
|
|
164
170
|
state.cost = sessionInfo.cost;
|
|
165
171
|
state.tokens = tokenUsage.input + tokenUsage.output + tokenUsage.reasoning + tokenUsage.cache.read + tokenUsage.cache.write;
|
|
@@ -177,7 +183,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
177
183
|
if (controller.signal.aborted && Date.now() - started >= timeoutSeconds * 1000) state.budgetReason = 'wall-time limit';
|
|
178
184
|
else if (controller.signal.aborted && signal?.aborted && /wall.?time/i.test(String(signal.reason?.message || signal.reason || ''))) state.budgetReason = 'wall-time limit';
|
|
179
185
|
else if (controller.signal.aborted && signal?.aborted) state.budgetReason = 'cancelled';
|
|
180
|
-
else state.budgetReason = 'native
|
|
186
|
+
else state.budgetReason = 'native Carthagent runtime request failed';
|
|
181
187
|
}
|
|
182
188
|
state.errors.push(error instanceof Error ? error.message : String(error));
|
|
183
189
|
if (session && !outcome) {
|
|
@@ -236,7 +242,21 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
|
|
|
236
242
|
}
|
|
237
243
|
if (!outcome && !state.budgetReason) {
|
|
238
244
|
state.budgetReason = 'missing terminal session outcome';
|
|
239
|
-
state.errors.push('
|
|
245
|
+
state.errors.push('Carthagent runtime session has no authoritative terminal outcome.');
|
|
240
246
|
}
|
|
241
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
...state,
|
|
249
|
+
outcome: outcome || null,
|
|
250
|
+
interruptionStatus,
|
|
251
|
+
exitCode: state.budgetReason || streamError ? 1 : 0,
|
|
252
|
+
timedOut: state.budgetReason === 'wall-time limit',
|
|
253
|
+
durationMs: Date.now() - started,
|
|
254
|
+
sessionID: session?.id || null,
|
|
255
|
+
runtime: NATIVE_EVALUATION_RUNTIME,
|
|
256
|
+
compatibilityVersion: version,
|
|
257
|
+
runtimeVersion: actualRuntimeVersion ?? null,
|
|
258
|
+
// Kept for consumers of the prior result shape; this is the actual server
|
|
259
|
+
// version, not the compatibility target supplied to the evaluator.
|
|
260
|
+
opencodeVersion: actualRuntimeVersion ?? null,
|
|
261
|
+
};
|
|
242
262
|
}
|
|
@@ -26,7 +26,7 @@ export async function runHeadless(service, workspace, prompt, options = {}) {
|
|
|
26
26
|
created = await createSession(service, workspace, { body: options.session ?? {}, signal });
|
|
27
27
|
id = created?.id ?? created?.sessionID;
|
|
28
28
|
}
|
|
29
|
-
if (!id) throw new Error('
|
|
29
|
+
if (!id) throw new Error('Carthagent runtime did not return a valid session identifier');
|
|
30
30
|
session = created ?? { id };
|
|
31
31
|
await setSessionEnvironment(service, id, options.env ?? process.env, { location: workspace, signal });
|
|
32
32
|
promptStarted = true;
|
|
@@ -123,18 +123,22 @@ async function incompleteResult(service, sessionID, workspace, accepted, session
|
|
|
123
123
|
const fetched = await sessionMessages(service, sessionID, { location: workspace, signal: AbortSignal.timeout(5000) }).catch(() => []);
|
|
124
124
|
const messages = currentTurnMessages(chronologicalMessages(fetched), accepted);
|
|
125
125
|
const idle = terminalIdle(messages);
|
|
126
|
-
|
|
126
|
+
// The session-level outcome may belong to a previously completed turn. Only
|
|
127
|
+
// an idle message bounded to this accepted prompt can establish a terminal
|
|
128
|
+
// outcome for the current turn.
|
|
129
|
+
const outcome = idle?.outcome;
|
|
127
130
|
const result = extractAssistant(messages, accepted);
|
|
128
131
|
const reasonText = session.ctg?.reason ?? reason;
|
|
129
132
|
const finalOutcome = outcome ?? (reason === 'timed-out' ? 'timed-out' : 'interrupted');
|
|
133
|
+
const { outcome: _priorOutcome, ...sessionWithoutPriorOutcome } = session;
|
|
130
134
|
return {
|
|
131
|
-
session: { ...
|
|
135
|
+
session: { ...sessionWithoutPriorOutcome, ...(outcome ? { outcome } : {}), error: session.error ?? idle?.error },
|
|
132
136
|
accepted,
|
|
133
137
|
messages,
|
|
134
138
|
result,
|
|
135
139
|
outcome: finalOutcome,
|
|
136
140
|
error: finalOutcome === 'failed' ? result?.error ?? session.error ?? idle?.error : undefined,
|
|
137
|
-
terminal: Boolean(
|
|
141
|
+
terminal: Boolean(idle),
|
|
138
142
|
reason: reasonText,
|
|
139
143
|
budgetReason: finalOutcome === 'timed-out' || finalOutcome === 'failed' ? reasonText : undefined,
|
|
140
144
|
cause: cause?.message,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const NATIVE_LOGIN_PROVIDERS = Object.freeze([
|
|
2
|
+
Object.freeze({ id: 'experiential-labs', name: 'Carthagent Ship', aliases: ['ship'], oauth: 'carthagent-device' }),
|
|
3
|
+
Object.freeze({ id: 'anthropic', name: 'Claude / Anthropic', aliases: ['claude', 'claudecode'] }),
|
|
4
|
+
Object.freeze({ id: 'xai', name: 'xAI / Grok', aliases: ['grok', 'xai-code'], oauth: 'device' }),
|
|
5
|
+
Object.freeze({ id: 'openai', name: 'Codex / OpenAI', aliases: ['codex', 'openai-codex'], oauth: 'chatgpt-token-sharing' }),
|
|
6
|
+
Object.freeze({ id: 'google', name: 'Gemini', aliases: ['gemini'] }),
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export function nativeLoginProvider(value) {
|
|
10
|
+
const key = String(value || '').trim().toLowerCase();
|
|
11
|
+
return NATIVE_LOGIN_PROVIDERS.find(provider => provider.id === key || provider.name.toLowerCase() === key || provider.aliases.includes(key));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nativeLoginProviders(integrations) {
|
|
15
|
+
return NATIVE_LOGIN_PROVIDERS.flatMap(provider => {
|
|
16
|
+
const integration = integrations.find(item => item.id === provider.id);
|
|
17
|
+
return integration ? [{ ...integration, name: provider.name }] : [];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function nativeProviderName(value) {
|
|
22
|
+
return nativeLoginProvider(value)?.name ?? String(value || '');
|
|
23
|
+
}
|