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.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- 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/launch-readiness-live-results.md +65 -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 +175 -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-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -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/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -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 +22 -5
- 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/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- 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 +243 -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 +65 -36
- 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 +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -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 +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -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 +64 -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/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -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 +55 -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 +525 -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 +33 -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-guidance.test.mjs +16 -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 +83 -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 +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -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 +248 -20
- package/tests/opencode-runtime.test.mjs +597 -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 +231 -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/tui.test.mjs +2 -2
- 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 +45 -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 +882 -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 +91 -65
- 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,222 @@
|
|
|
1
|
+
/** Shared native update operation for CLI and interactive callers. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { compareVersions, cleanStaleGlobalInstall, detectInstallMethod as detectDefaultInstallMethod, fetchLatestRelease, npmInstallSpec, packageVersion } from '../update.mjs';
|
|
5
|
+
import { parseUpdateOptions, readUpdateChannel, writeUpdateChannel, releaseAllowedOnChannel } from '../update-channel.mjs';
|
|
6
|
+
import { withoutRuntimeGithubToken } from '../environment.mjs';
|
|
7
|
+
|
|
8
|
+
const SEMVER_TAG = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
10
|
+
|
|
11
|
+
/** Run a bounded child process and retain only a bounded combined output tail. */
|
|
12
|
+
export function runUpdateCommand(argv, { cwd, timeoutSeconds = 600, env = process.env, signal, spawnImpl = spawn, platform = process.platform } = {}) {
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
let output = '';
|
|
15
|
+
let timedOut = false;
|
|
16
|
+
let settled = false;
|
|
17
|
+
if (signal?.aborted) { resolve({ code: 1, output: String(signal.reason ?? 'aborted'), timedOut: false, cancelled: true }); return; }
|
|
18
|
+
const child = spawnImpl(argv[0], argv.slice(1), { cwd, env: withoutRuntimeGithubToken(env), windowsHide: true, detached: platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
19
|
+
const append = chunk => {
|
|
20
|
+
output = (output + chunk.toString()).slice(-MAX_OUTPUT);
|
|
21
|
+
};
|
|
22
|
+
child.stdout.on('data', append);
|
|
23
|
+
child.stderr.on('data', append);
|
|
24
|
+
let hardStop;
|
|
25
|
+
let killTimer;
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
const stop = (force = false) => {
|
|
28
|
+
try {
|
|
29
|
+
if (platform !== 'win32' && child.pid) process.kill(-child.pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
30
|
+
else {
|
|
31
|
+
if (platform === 'win32' && child.pid) {
|
|
32
|
+
const killer = spawnImpl('taskkill', ['/pid', String(child.pid), '/t', ...(force ? ['/f'] : [])], {
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
env: withoutRuntimeGithubToken(env),
|
|
35
|
+
stdio: 'ignore',
|
|
36
|
+
});
|
|
37
|
+
killer.on('error', () => {});
|
|
38
|
+
killer.unref();
|
|
39
|
+
}
|
|
40
|
+
child.kill(force ? 'SIGKILL' : 'SIGTERM');
|
|
41
|
+
}
|
|
42
|
+
} catch { /* already exited */ }
|
|
43
|
+
};
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
stop();
|
|
47
|
+
killTimer = setTimeout(() => stop(true), 1500);
|
|
48
|
+
killTimer.unref?.();
|
|
49
|
+
};
|
|
50
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
if (signal?.aborted) onAbort();
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
timedOut = true;
|
|
54
|
+
stop();
|
|
55
|
+
hardStop = setTimeout(() => stop(true), 5000);
|
|
56
|
+
hardStop.unref?.();
|
|
57
|
+
}, Math.max(1, timeoutSeconds) * 1000);
|
|
58
|
+
timer.unref?.();
|
|
59
|
+
const finish = code => {
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
clearTimeout(hardStop);
|
|
64
|
+
clearTimeout(killTimer);
|
|
65
|
+
signal?.removeEventListener('abort', onAbort);
|
|
66
|
+
resolve({ code: Number.isInteger(code) ? code : 1, output, timedOut, cancelled });
|
|
67
|
+
};
|
|
68
|
+
child.on('error', error => { append(error.message); finish(1); });
|
|
69
|
+
child.on('close', finish);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function npmArgv(args, platform = process.platform) {
|
|
74
|
+
// npm is a .cmd shim on Windows. Keep arguments as argv rather than a shell string.
|
|
75
|
+
return platform === 'win32' ? ['cmd.exe', '/d', '/s', '/c', 'npm', ...args] : ['npm', ...args];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emit(io, level, message) {
|
|
79
|
+
const fn = io?.[level] || io?.log;
|
|
80
|
+
if (typeof fn === 'function') fn.call(io, message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function commandSucceeded(result) {
|
|
84
|
+
return result?.code === 0 && result.timedOut !== true && result.cancelled !== true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function commandFailure(label, result) {
|
|
88
|
+
const status = result?.timedOut ? ' (timed out)' : result?.cancelled ? ' (cancelled)' : '';
|
|
89
|
+
const detail = result?.output?.trim();
|
|
90
|
+
return new Error(`${label} failed${status}${detail ? `:\n${detail}` : ''}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validatedRelease(release) {
|
|
94
|
+
if (!release || typeof release !== 'object') throw new Error('Latest release response is invalid.');
|
|
95
|
+
const version = String(release.version || '');
|
|
96
|
+
const tag = String(release.tag || '');
|
|
97
|
+
if (!SEMVER_TAG.test(tag) || !SEMVER_TAG.test(version) || tag.replace(/^v/, '') !== version.replace(/^v/, '')) {
|
|
98
|
+
throw new Error('Latest release has an invalid or mismatched version tag.');
|
|
99
|
+
}
|
|
100
|
+
const expectedSpec = `github:B-icy/Carthagent#${tag}`;
|
|
101
|
+
if (release.installSpec !== expectedSpec) throw new Error('Latest release install spec is not pinned to its validated release tag.');
|
|
102
|
+
return { ...release, version, tag, installSpec: expectedSpec };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve and apply an update without terminating the host process.
|
|
107
|
+
*
|
|
108
|
+
* Returns `{status, release, method}` where status is `updated`, `current`,
|
|
109
|
+
* `ahead`, or `available` (`--check`). Failures throw after a bounded command
|
|
110
|
+
* reports a non-zero exit, allowing CLI/UI hosts to choose their own UX.
|
|
111
|
+
*/
|
|
112
|
+
export async function runNativeUpdate({
|
|
113
|
+
args = [], io = console, cwd = process.cwd(), root = cwd,
|
|
114
|
+
fetchLatestRelease: fetchRelease = fetchLatestRelease,
|
|
115
|
+
detectInstallMethod = detectDefaultInstallMethod,
|
|
116
|
+
runCommand = runUpdateCommand,
|
|
117
|
+
cleanStaleInstall = cleanStaleGlobalInstall,
|
|
118
|
+
currentVersion,
|
|
119
|
+
platform = process.platform,
|
|
120
|
+
nodeVersion = process.versions.node,
|
|
121
|
+
signal,
|
|
122
|
+
allowCloneUpdate = true,
|
|
123
|
+
env = process.env,
|
|
124
|
+
} = {}) {
|
|
125
|
+
signal?.throwIfAborted();
|
|
126
|
+
const options = parseUpdateOptions(args);
|
|
127
|
+
const { checkOnly, force } = options;
|
|
128
|
+
const channel = options.channel ?? readUpdateChannel(env);
|
|
129
|
+
const remember = () => { if (options.channel !== null) writeUpdateChannel(channel, env); };
|
|
130
|
+
const resolved = await fetchRelease({ signal, channel, env });
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
const release = validatedRelease(resolved?.release ?? resolved);
|
|
133
|
+
if (!releaseAllowedOnChannel(release, channel)) throw Error(`Release is not permitted on the ${channel} channel.`);
|
|
134
|
+
const installed = currentVersion || packageVersion();
|
|
135
|
+
if (!installed) throw new Error('Cannot determine the installed Carthagent version.');
|
|
136
|
+
const delta = compareVersions(installed, release.version);
|
|
137
|
+
if (release.minNode && compareVersions(nodeVersion, release.minNode) < 0) {
|
|
138
|
+
throw new Error(`Carthagent ${release.version} requires Node ≥ ${release.minNode} — this runtime is ${nodeVersion}`);
|
|
139
|
+
}
|
|
140
|
+
if (delta === 0 && !force) {
|
|
141
|
+
if (!checkOnly) remember();
|
|
142
|
+
emit(io, 'log', `Carthagent ${installed} is already the latest release.`);
|
|
143
|
+
return { status: 'current', release };
|
|
144
|
+
}
|
|
145
|
+
if (delta > 0 && !force && options.channel === null) {
|
|
146
|
+
emit(io, 'log', `Installed ${installed} is ahead of latest release ${release.version} — pass --force to reinstall anyway.`);
|
|
147
|
+
return { status: 'ahead', release };
|
|
148
|
+
}
|
|
149
|
+
if (checkOnly) {
|
|
150
|
+
emit(io, 'log', `Carthagent ${release.version} is available${release.note ? ` — ${release.note}` : ''} (installed: ${installed}) — run ctg update${options.channel === null ? '' : ` --channel ${channel}`}`);
|
|
151
|
+
return { status: 'available', release };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const method = detectInstallMethod(root);
|
|
155
|
+
signal?.throwIfAborted();
|
|
156
|
+
emit(io, 'log', `Updating Carthagent ${installed} → ${release.version} (${method} install)`);
|
|
157
|
+
const run = async (argv, label, timeoutSeconds = 600) => {
|
|
158
|
+
signal?.throwIfAborted();
|
|
159
|
+
const result = await runCommand(argv, { cwd: root, timeoutSeconds, ...(signal ? { signal } : {}) });
|
|
160
|
+
if (!commandSucceeded(result)) throw commandFailure(label, result);
|
|
161
|
+
return result;
|
|
162
|
+
};
|
|
163
|
+
const installGlobal = async (argv, label) => {
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
let result = await runCommand(argv, { cwd: root, timeoutSeconds: 600, ...(signal ? { signal } : {}) });
|
|
166
|
+
if (!commandSucceeded(result) && !result?.timedOut && !result?.cancelled && /\bENOTDIR\b|\bEEXIST\b|\bENOTEMPTY\b/.test(result?.output || '')) {
|
|
167
|
+
const probe = await runCommand(npmArgv(['root', '-g'], platform), { cwd: root, timeoutSeconds: 30, ...(signal ? { signal } : {}) });
|
|
168
|
+
const removed = commandSucceeded(probe) ? cleanStaleInstall(probe.output.trim()) : [];
|
|
169
|
+
if (removed.length) {
|
|
170
|
+
emit(io, 'log', `Removed stale global install ${removed.length === 1 ? 'entry' : 'entries'} — retrying.`);
|
|
171
|
+
result = await runCommand(argv, { cwd: root, timeoutSeconds: 600, ...(signal ? { signal } : {}) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!commandSucceeded(result)) throw commandFailure(label, result);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (method === 'clone') {
|
|
178
|
+
if (!allowCloneUpdate) throw new Error('Native update refuses to checkout a release into the active source tree. Install Carthagent as an npm package or use `ctg update --check`.');
|
|
179
|
+
signal?.throwIfAborted();
|
|
180
|
+
const dirty = await runCommand(['git', 'status', '--porcelain'], { cwd: root, timeoutSeconds: 30, ...(signal ? { signal } : {}) });
|
|
181
|
+
if (!commandSucceeded(dirty)) throw new Error(`Cannot inspect the clone at ${root}${dirty?.output ? `: ${dirty.output.trim()}` : dirty?.timedOut ? ' (timed out)' : dirty?.cancelled ? ' (cancelled)' : ''}`);
|
|
182
|
+
if (dirty.output?.trim()) throw new Error(`The clone at ${root} has uncommitted changes. Commit, stash, or update it manually.`);
|
|
183
|
+
// Fetch this specific remote tag into a fresh private ref. This avoids
|
|
184
|
+
// trusting a pre-existing local tag and never overwrites user's local tags.
|
|
185
|
+
const fetchedRef = `refs/opencode-update/${randomUUID()}`;
|
|
186
|
+
let fetchAttempted = false;
|
|
187
|
+
try {
|
|
188
|
+
fetchAttempted = true;
|
|
189
|
+
await run(['git', 'fetch', '--no-tags', 'origin', `refs/tags/${release.tag}:${fetchedRef}`], `git fetch ${release.tag}`, 120);
|
|
190
|
+
const resolved = await run(['git', 'rev-parse', '--verify', `${fetchedRef}^{commit}`], `resolve fetched tag ${release.tag}`, 30);
|
|
191
|
+
const commit = resolved.output.trim();
|
|
192
|
+
if (!/^[0-9a-f]{40,64}$/i.test(commit)) throw new Error(`Could not resolve fetched release tag ${release.tag} to a commit.`);
|
|
193
|
+
await run(['git', 'checkout', '--detach', commit], `git checkout ${release.tag}`, 60);
|
|
194
|
+
} finally {
|
|
195
|
+
// Deleting our uniquely generated ref is best-effort and cannot touch
|
|
196
|
+
// any existing user tag or branch. Attempt it even when fetch reports
|
|
197
|
+
// cancellation: the remote write may have completed before termination.
|
|
198
|
+
if (fetchAttempted) {
|
|
199
|
+
try { await runCommand(['git', 'update-ref', '-d', fetchedRef], { cwd: root, timeoutSeconds: 30 }); }
|
|
200
|
+
catch { /* best-effort cleanup must not replace the original cancellation or update error */ }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await run(npmArgv(['ci'], platform), 'npm ci');
|
|
204
|
+
await installGlobal(npmArgv(['install', '-g', '.'], platform), 'npm install -g .');
|
|
205
|
+
} else if (method === 'npm') {
|
|
206
|
+
const installSpec = await npmInstallSpec(release, async registrySpec => {
|
|
207
|
+
const result = await runCommand(npmArgv(['view', registrySpec, 'version', '--json'], platform), {
|
|
208
|
+
cwd: root, timeoutSeconds: 10, ...(signal ? { signal } : {}),
|
|
209
|
+
});
|
|
210
|
+
signal?.throwIfAborted();
|
|
211
|
+
return commandSucceeded(result);
|
|
212
|
+
});
|
|
213
|
+
await installGlobal(npmArgv(['install', '-g', '--install-links=true', installSpec], platform), `npm install ${installSpec}`);
|
|
214
|
+
} else {
|
|
215
|
+
throw new Error(`Unsupported install method: ${method}`);
|
|
216
|
+
}
|
|
217
|
+
remember();
|
|
218
|
+
emit(io, 'log', `Carthagent updated to ${release.version}. Restart any running console to pick it up.`);
|
|
219
|
+
return { status: 'updated', release, method };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { npmArgv as npmUpdateArgv };
|
|
@@ -12,7 +12,7 @@ const activeOperations = new Set();
|
|
|
12
12
|
|
|
13
13
|
/** @param {unknown} value @returns {TaskState} */
|
|
14
14
|
function validState(value) {
|
|
15
|
-
if (!value || typeof value !== 'object' || !('version' in value) || value.version !== 1 || !('tasks' in value) || !Array.isArray(value.tasks) || !('operations' in value) || !Array.isArray(value.operations)) throw Error('Invalid CTG
|
|
15
|
+
if (!value || typeof value !== 'object' || !('version' in value) || value.version !== 1 || !('tasks' in value) || !Array.isArray(value.tasks) || !('operations' in value) || !Array.isArray(value.operations)) throw Error('Invalid CTG Carthagent runtime workspace state');
|
|
16
16
|
return /** @type {TaskState} */ (value);
|
|
17
17
|
}
|
|
18
18
|
|
package/lib/planning-advice.mjs
CHANGED
|
@@ -19,10 +19,10 @@ export async function inspectAdvice(state, capture, { enabled = false, createAdv
|
|
|
19
19
|
if (prior) return { ...structuredClone(prior), captureId: capture.id, reused: true };
|
|
20
20
|
if (attempts.length >= 3) return { mode: 'limit', authority: 'none', captureId: capture.id, instruction: 'Three advice attempts used; continue ordinary adversarial review.' };
|
|
21
21
|
const attempt = { key, captureId: capture.id, runId: capture.runId, revision: capture.revision, digest: capture.digest, fingerprint: capture.fingerprint, at: new Date().toISOString(), mode: 'unavailable', authority: 'none', instruction: 'Attempt reserved; if interrupted, do not retry automatically. Ordinary review remains required.' };
|
|
22
|
-
attempts.push(attempt); persist(); // reserve durably before creating transport/dispatch
|
|
22
|
+
attempts.push(attempt); await persist(); // reserve durably before creating transport/dispatch
|
|
23
23
|
let result;
|
|
24
24
|
try { result = await advisePlan(capture.plan, { advisor: createAdvisor() }); }
|
|
25
25
|
catch { result = { mode: 'unavailable', authority: 'none', findings: [], error: 'Jev configuration unavailable; ordinary review remains required.' }; }
|
|
26
26
|
Object.assign(attempt, result, { instruction: 'Investigate only flagged topics (probability >= 0.5) and state dispositions in the normal review. Low scores are not flags, zero flags are not proof. Advice cannot approve or resolve blockers.' });
|
|
27
|
-
persist(); return structuredClone(attempt);
|
|
27
|
+
await persist(); return structuredClone(attempt);
|
|
28
28
|
}
|
|
@@ -64,11 +64,11 @@ const EXPERIENTIAL_MODEL_PROFILES = Object.freeze({
|
|
|
64
64
|
'glm-5.3-flash': { name: 'GLM 5.3 Flash', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text', 'image'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
|
|
65
65
|
'glm-5.3': { name: 'GLM 5.3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
|
|
66
66
|
'kimi-k3': { name: 'Kimi K3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsDeveloperRole: false, supportsReasoningEffort: true, thinkingFormat: 'deepseek' } },
|
|
67
|
-
'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true } },
|
|
67
|
+
'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, reasoningField: 'reasoning_content', requireReasoning: true, reasoningPlaceholder: ' ' } },
|
|
68
68
|
'mercury-3': { name: 'Mercury 3', reasoning: true, thinkingLevelMap: MERCURY3_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 131_072, input: ['text'], compat: { supportsReasoningEffort: true } },
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat'];
|
|
71
|
+
const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat', 'cost'];
|
|
72
72
|
|
|
73
73
|
export const EXPERIENTIAL_PROVIDER_ID = PROVIDER_ID;
|
|
74
74
|
export const EXPERIENTIAL_PROVIDER_NAME = CARTHAGENT_SHIP_PROVIDER_NAME;
|
package/lib/providers/names.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CARTHAGENT_SHIP_PROVIDER_ID = 'experiential-labs';
|
|
2
|
-
export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent
|
|
2
|
+
export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent Cloud';
|
|
3
3
|
|
|
4
4
|
export function providerDisplayName(providerId) {
|
|
5
5
|
return providerId === CARTHAGENT_SHIP_PROVIDER_ID ? CARTHAGENT_SHIP_PROVIDER_NAME : String(providerId || '');
|
package/lib/tui/framing.mjs
CHANGED
|
@@ -1,80 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Behind-the-scenes phased-delivery framing (the carthagent default flow).
|
|
3
|
-
*
|
|
4
|
-
* Every typed task is a phased delivery run: the raw text is what the user
|
|
5
|
-
* sees echoed in the feed, while the engine receives the expanded delivery template
|
|
6
|
-
* body. Resolving the template here (rather than forwarding "/guide <task>"
|
|
7
|
-
* to the engine) keeps the behavior identical in --isolate and headless -p modes,
|
|
8
|
-
* and keeps the machinery invisible in the feed.
|
|
9
|
-
*
|
|
10
|
-
* This is the default flow for carthagent — the whole product is built around it —
|
|
11
|
-
* so the input pill stays empty and the hint rotates through generic
|
|
12
|
-
* builder one-liners.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { join, dirname, resolve } from 'node:path';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const ROOT = resolve(__dirname, '..', '..');
|
|
20
|
-
const TEMPLATE_PATH = join(ROOT, 'prompts', 'delivery.md');
|
|
21
|
-
|
|
22
|
-
let cached = null;
|
|
23
|
-
|
|
24
|
-
export const FRAME_HINTS = ['Message carthagent…'];
|
|
25
|
-
|
|
26
|
-
export function frameHint() {
|
|
27
|
-
return FRAME_HINTS[0];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readTemplate(path) {
|
|
31
|
-
let body = '';
|
|
32
|
-
try {
|
|
33
|
-
const raw = readFileSync(path, 'utf8');
|
|
34
|
-
const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
35
|
-
body = (fm ? raw.slice(fm[0].length) : raw).trim();
|
|
36
|
-
} catch { body = ''; }
|
|
37
|
-
return body;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Body of prompts/delivery.md with frontmatter stripped. '' when unreadable. */
|
|
41
|
-
export function framingTemplate(path = TEMPLATE_PATH) {
|
|
42
|
-
if (path === TEMPLATE_PATH) {
|
|
43
|
-
if (cached == null) cached = readTemplate(TEMPLATE_PATH);
|
|
44
|
-
return cached;
|
|
45
|
-
}
|
|
46
|
-
return readTemplate(path);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Expand the delivery template for a task. Falls back to the bare task text. */
|
|
50
|
-
export function framePrompt(task, template = framingTemplate()) {
|
|
51
|
-
const t = String(task).trim();
|
|
52
|
-
if (!template) return t;
|
|
53
|
-
return template.includes('$@') ? template.replaceAll('$@', t) : `${template}\n\n${t}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Recover the original task text from a framed prompt, else null. */
|
|
57
|
-
export function unframe(text) {
|
|
58
|
-
// Match the current template suffix first: the task itself may contain
|
|
59
|
-
// blank lines (or even phase headings). Keep the legacy format fallback.
|
|
60
|
-
const value = String(text);
|
|
61
|
-
const template = framingTemplate();
|
|
62
|
-
const parts = template.split('$@');
|
|
63
|
-
if (parts.length === 2 && value.startsWith(parts[0]) && value.endsWith(parts[1])) {
|
|
64
|
-
return value.slice(parts[0].length, parts[1] ? -parts[1].length : undefined).trim();
|
|
65
|
-
}
|
|
66
|
-
const m = value.match(/^Deliver this task in phases(?: with executable evidence)?:\s*([\s\S]*?)\n\n/);
|
|
67
|
-
return m ? m[1].trim() : null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** True when a submitted prompt should get the delivery framing. */
|
|
71
|
-
export function shouldFrame(text, { enabled = true, delivery = true } = {}) {
|
|
72
|
-
if (!enabled || !delivery) return false;
|
|
73
|
-
const v = String(text).trim();
|
|
74
|
-
if (!v) return false;
|
|
75
|
-
if (v.startsWith('/') || v.startsWith('!')) return false;
|
|
76
|
-
// Questions/read-only asks are not delivery runs.
|
|
77
|
-
if (v.endsWith('?') && v.length < 300) return false;
|
|
78
|
-
if (/^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) && v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v)) return false;
|
|
79
|
-
return existsSync(TEMPLATE_PATH);
|
|
80
|
-
}
|
|
1
|
+
export { FRAME_HINTS, frameHint, framingTemplate, framePrompt, unframe, shouldFrame } from '../../plugins/carthagent/src/server/framing.mjs';
|
package/lib/tui/models.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function providerForModel(model, list) {
|
|
|
30
30
|
export function modelMatchesQuery(model, query) {
|
|
31
31
|
const tokens = String(query || '').toLowerCase().match(/[\p{L}\p{N}]+/gu) || [];
|
|
32
32
|
if (!tokens.length) return true;
|
|
33
|
-
const searchable = [model?.provider, providerDisplayName(model?.provider), model?.id, model?.full, model?.name]
|
|
33
|
+
const searchable = [model?.provider, providerDisplayName(model?.provider), model?.provider === 'experiential-labs' ? 'Carthagent Ship' : '', model?.id, model?.full, model?.name]
|
|
34
34
|
.filter(Boolean)
|
|
35
35
|
.join(' ')
|
|
36
36
|
.toLowerCase();
|
package/lib/update.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Install methods:
|
|
11
11
|
* - clone : the package root contains .git (installed via `npm install -g .` from a
|
|
12
12
|
* clone) → checkout the tag in place, `npm ci`, re-link.
|
|
13
|
-
* - npm : global
|
|
13
|
+
* - npm : global package install, preferring the exact npm registry version.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -151,6 +151,15 @@ export function detectInstallMethod(root) {
|
|
|
151
151
|
return existsSync(join(root, '.git')) ? 'clone' : 'npm';
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** Prefer an exact registry release when available; retain the validated GitHub fallback. */
|
|
155
|
+
export async function npmInstallSpec(release, npmView) {
|
|
156
|
+
const registrySpec = `carthagent@${release.version}`;
|
|
157
|
+
try {
|
|
158
|
+
if (await npmView(registrySpec)) return registrySpec;
|
|
159
|
+
} catch { /* registry unavailable or version not published */ }
|
|
160
|
+
return release.installSpec;
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
/**
|
|
155
164
|
* Entries under a global node_modules dir that break `npm install -g` with
|
|
156
165
|
* ENOTDIR/EEXIST: a non-directory `carthagent` entry (stray file or dangling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carthagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.2",
|
|
4
4
|
"workspaces": [
|
|
5
5
|
"plugins/carthagent"
|
|
6
6
|
],
|
|
@@ -16,17 +16,18 @@
|
|
|
16
16
|
"evidence-driven-delivery"
|
|
17
17
|
],
|
|
18
18
|
"bin": {
|
|
19
|
-
"ctg": "
|
|
19
|
+
"ctg": "bin/ctg.mjs"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/B-icy/Carthagent"
|
|
23
|
+
"url": "git+https://github.com/B-icy/Carthagent.git"
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://github.com/B-icy/Carthagent#readme",
|
|
26
26
|
"bugs": "https://github.com/B-icy/Carthagent/issues",
|
|
27
27
|
"files": [
|
|
28
28
|
"bin",
|
|
29
29
|
"docs",
|
|
30
|
+
"script/ctg/build-owned-runtime.sh",
|
|
30
31
|
"extensions",
|
|
31
32
|
"guidance",
|
|
32
33
|
"lib",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"tests/*.mjs",
|
|
44
45
|
"tests/*.py",
|
|
45
46
|
"tests/fixtures/archive-plugin",
|
|
47
|
+
"tests/fixtures/native-discovery-plugin/server.ts",
|
|
46
48
|
"tools",
|
|
47
49
|
"vendor",
|
|
48
50
|
"evaluate.mjs",
|
|
@@ -60,29 +62,53 @@
|
|
|
60
62
|
"lint": "eslint . && npm run typecheck",
|
|
61
63
|
"quality": "npm run lint && npm run build && npm test",
|
|
62
64
|
"test:browser": "node tools/test-webdriver.mjs",
|
|
63
|
-
"test:native-host": "node tools/test-native-host.mjs",
|
|
65
|
+
"test:native-host": "node tools/test-native-host.mjs && node tools/test-native-ship-models.mjs",
|
|
66
|
+
"test:native-ship-models": "node tools/test-native-ship-models.mjs",
|
|
67
|
+
"test:native-product": "node tools/test-native-product.mjs",
|
|
68
|
+
"test:native-tasks": "node tools/test-native-tasks.mjs",
|
|
69
|
+
"test:native-review": "node tools/test-native-review.mjs",
|
|
70
|
+
"test:native-report": "node tools/test-native-report.mjs",
|
|
71
|
+
"test:native-runtime": "node tools/test-native-runtime.mjs",
|
|
72
|
+
"test:native-navigation": "cd plugins/carthagent && bun test --preload @opentui/solid/preload --timeout 30000 tests/voyages-navigation.test.tsx",
|
|
73
|
+
"test:native-performance": "node tools/test-native-performance.mjs",
|
|
74
|
+
"test:native-terminal-performance": "python3 tools/test-native-terminal-performance.py",
|
|
64
75
|
"test:native-package": "node tools/test-native-package.mjs",
|
|
65
76
|
"test:native-controls": "node tools/test-native-controls.mjs",
|
|
77
|
+
"test:native-ship-walkthrough": "node tools/test-native-ship-walkthrough.mjs",
|
|
78
|
+
"test:native-default-cli": "node tools/test-native-default-cli.mjs",
|
|
79
|
+
"test:owned-host-controls": "node tools/test-owned-host-controls.mjs",
|
|
80
|
+
"test:owned-visual-clear-env": "python3 tools/test-owned-visual-clear-env.py",
|
|
81
|
+
"test:native-terminal-semantics": "python3 tools/native-terminal-semantics.py",
|
|
82
|
+
"test:native-glyph-pty": "node tools/test-native-glyph-pty.mjs",
|
|
66
83
|
"test:native-outcomes": "node tools/test-native-outcomes.mjs",
|
|
67
84
|
"test:native-pty": "node tools/test-native-pty.mjs",
|
|
85
|
+
"test:owned-opencode": "node tools/test-owned-opencode-runtime.mjs",
|
|
68
86
|
"build": "npm run check && npm run typecheck",
|
|
69
87
|
"test": "node --test tests/*.test.mjs",
|
|
70
|
-
"test:python": "node tools/test-python.mjs"
|
|
88
|
+
"test:python": "node tools/test-python.mjs",
|
|
89
|
+
"test:native-slash-commands": "node tools/test-native-slash-commands.mjs",
|
|
90
|
+
"manual:prepare": "node tools/prepare-native-manual.mjs",
|
|
91
|
+
"manual:run": "node tools/native-manual.mjs"
|
|
71
92
|
},
|
|
72
93
|
"dependencies": {
|
|
73
|
-
"@opencode/cli": "2.0.32",
|
|
74
94
|
"@opencode/client": "2.0.32",
|
|
75
95
|
"@opencode/plugin": "2.0.32",
|
|
96
|
+
"@opencode/theme": "2.0.32",
|
|
97
|
+
"@opentui/core": "0.5.17",
|
|
98
|
+
"@opentui/solid": "0.5.17",
|
|
76
99
|
"express": "^5.2.1",
|
|
77
100
|
"jiti": "^2.7.0",
|
|
78
101
|
"jsdom": "^30.0.1",
|
|
102
|
+
"solid-js": "1.9.12",
|
|
79
103
|
"tree-sitter-wasms": "^0.1.13",
|
|
80
|
-
"web-tree-sitter": "
|
|
104
|
+
"web-tree-sitter": "0.25.10",
|
|
105
|
+
"web-tree-sitter-legacy": "npm:web-tree-sitter@0.22.6"
|
|
81
106
|
},
|
|
82
107
|
"overrides": {
|
|
83
108
|
"seroval": "1.6.9"
|
|
84
109
|
},
|
|
85
110
|
"devDependencies": {
|
|
111
|
+
"@opencode/cli": "2.0.32",
|
|
86
112
|
"@eslint/js": "^10.0.1",
|
|
87
113
|
"@types/node": "^24.3.0",
|
|
88
114
|
"eslint": "^10.11.0",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Carthagent OpenCode TUI plugin
|
|
2
|
+
|
|
3
|
+
This is the V2.0.32-native Carthagent TUI host integration. OpenCode owns the terminal renderer, input editor, session lifecycle, model selectors, dialogs, route navigation, and command palette; this package adds Carthagent commands, workspace presentation, status contributions, and a matching semantic theme.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
The slash/palette inventory lives in `src/ui/commands.ts`. The plugin registers `/help` and `/ctg-help` to show Carthagent commands; the latter is the unambiguous alias if a host context claims `/help`. Both are covered by the pinned OpenCode 2.0.32 PTY smoke test. `/themes` and `/settings` remain native. The plugin leaves `/new` host-owned. CTG uses `/ctg-*` only for CTG-specific operations. `/model`, `/compact`, and `/abort` preserve their historical command names while providing provider-aware selection, notes-aware compaction and interruption. `/guide` toggles per-session framing when bare and sends the canonical delivery-framed task when given an argument. `/raw` tags that prompt with `ctg.raw` so the server bypasses framing without changing execution policy. Bare `/review` invokes the workspace server's independent reviewer, never the active session; `/review ask|yes|no` configures the automatic-review default in the Carthagent profile only, and does not disable the bare command. `/workflow`, `/architecture`, and `/evidence` render the selected session's authoritative workflow graph, architecture nodes/edges/ports, and bounded plan/evidence receipts, refreshing on native session/attention and server reconnect events. `/restart` interrupts only the selected session.
|
|
8
|
+
|
|
9
|
+
`/copy` uses the native renderer's OSC 52 clipboard operation when supported. Unsupported terminals get a clear error and the response remains selectable. `/export [file]` writes a standalone HTML transcript into the active project directory and refuses to overwrite an existing file.
|
|
10
|
+
|
|
11
|
+
## Workspace RPC
|
|
12
|
+
|
|
13
|
+
The client imports the shared contracts from `../workspace-rpc.js` and `../session-rpc.js`; it does not duplicate RPC definitions. Each RPC call supplies the selected OpenCode location and a bounded abort timeout. Workspace event subscriptions are scoped to component lifetime, and events only invalidate data: the page refetches a complete snapshot after an event and on `server.connected`. Native session data is merged with the CTG snapshot only for sessions associated with the snapshot's projects; unrelated OpenCode sessions are not presented as CTG sessions. Workspace navigation actions are also available through `/ctg-open`, `/ctg-task`, and `/ctg-worktree` for keyboard-first use.
|
|
14
|
+
|
|
15
|
+
## Host compatibility
|
|
16
|
+
|
|
17
|
+
`/theme [name]` provisions the selected compatibility theme as OpenCode-native semantic theme JSON in the active native profile's `themes/` directory, then writes the selection to `cli.json` while preserving unrelated settings. Since OpenCode discovers theme files at startup, a newly installed theme requires one restart; selecting an already installed theme only changes `cli.json` and uses the host's live config reload. Launchers can call the standalone `provisionDefaultNativeTheme(configDirectory)` export from `src/ui/theme-provision.mjs` before starting OpenCode so `carthage` is available on the first launch. Provisioning does not overwrite an existing user-owned theme file. `/clear` clears only the visible transcript view using the host presentation API; durable session history is unchanged. `/autocompact [on|off]` persists an explicit, idempotent per-session setting; bare `/autocompact` toggles it.
|
|
18
|
+
|
|
19
|
+
`/compact [notes]` uses the shared `carthagent.session.compact` RPC. The server stores notes for the compaction hook and reports whether the request was admitted; the native `session.compact` API itself does not accept `customInstructions`.
|
|
20
|
+
|
|
21
|
+
## Theme
|
|
22
|
+
|
|
23
|
+
`themes/carthage.json` uses the complete OpenCode V2 semantic hue/token structure and the approved Carthagent purple/gold identity. Remaining legacy palette names are converted to native OpenCode semantic tokens and hue scales by `src/ui/theme-provision.mjs` when provisioned. The default launcher provisioner is a separate export so the parent can invoke it without loading or editing the runtime entrypoint.
|
|
@@ -21,7 +21,7 @@ export default Plugin.define({
|
|
|
21
21
|
const health = result as { hostVersion: string; directory: string };
|
|
22
22
|
context.ui.toast.show({
|
|
23
23
|
title: 'Carthagent',
|
|
24
|
-
message: `
|
|
24
|
+
message: `Carthagent ${health.hostVersion} · ${context.ui.format.path(health.directory)}`,
|
|
25
25
|
variant: 'success',
|
|
26
26
|
});
|
|
27
27
|
} catch (error) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure, browser/native-safe counterpart of the backend's selected-evidence
|
|
2
|
+
// policy. This validates shape/bytes, not truth, secrets, or assessment quality.
|
|
3
|
+
const invalid = () => new Error('invalid_daily_evidence');
|
|
4
|
+
const bytes = value => new TextEncoder().encode(value).byteLength;
|
|
5
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
6
|
+
const hex = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
|
|
7
|
+
function exact(value, keys) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
9
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
10
|
+
|| Reflect.ownKeys(value).length !== keys.length
|
|
11
|
+
|| keys.some(key => {
|
|
12
|
+
const d = Object.getOwnPropertyDescriptor(value, key);
|
|
13
|
+
return !d?.enumerable || !Object.hasOwn(d, 'value');
|
|
14
|
+
})) throw invalid();
|
|
15
|
+
}
|
|
16
|
+
function text(value, limit) {
|
|
17
|
+
if (typeof value !== 'string' || bytes(value) > limit
|
|
18
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\p{Cs}]/u.test(value)) throw invalid();
|
|
19
|
+
return value.replace(/\r\n?/g, '\n').trim();
|
|
20
|
+
}
|
|
21
|
+
function identifier(value, pattern) {
|
|
22
|
+
if (typeof value !== 'string' || !pattern.test(value)) throw invalid();
|
|
23
|
+
return value.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function list(value, limit) {
|
|
26
|
+
if (!Array.isArray(value) || value.length > limit || Reflect.ownKeys(value).length !== value.length + 1) throw invalid();
|
|
27
|
+
for (let i = 0; i < value.length; i++) {
|
|
28
|
+
if (!Object.hasOwn(Object.getOwnPropertyDescriptor(value, String(i)) ?? {}, 'value')) throw invalid();
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
export function canonicalDailyEvidence(value) {
|
|
33
|
+
exact(value, ['projectId', 'outcomeId', 'runId', 'revision', 'goal', 'change', 'checks', 'findings']);
|
|
34
|
+
const revision = identifier(value.revision, hex);
|
|
35
|
+
const canonical = {
|
|
36
|
+
projectId: identifier(value.projectId, uuid), outcomeId: identifier(value.outcomeId, uuid), runId: identifier(value.runId, uuid), revision,
|
|
37
|
+
goal: text(value.goal, 1024), change: text(value.change, 3072),
|
|
38
|
+
checks: list(value.checks, 12).map(check => {
|
|
39
|
+
exact(check, ['name', 'before', 'after', 'revision', 'result']);
|
|
40
|
+
if (!['failed', 'not_run'].includes(check.before) || !['passed', 'failed', 'not_run'].includes(check.after)) throw invalid();
|
|
41
|
+
const checkedRevision = identifier(check.revision, hex);
|
|
42
|
+
if (check.after === 'passed' && checkedRevision !== revision) throw invalid();
|
|
43
|
+
return { name: text(check.name, 160), before: check.before, after: check.after, revision: checkedRevision, result: text(check.result, 1024) };
|
|
44
|
+
}),
|
|
45
|
+
findings: list(value.findings, 5).map(finding => {
|
|
46
|
+
exact(finding, ['issue', 'evidence', 'resolution']);
|
|
47
|
+
return { issue: text(finding.issue, 768), evidence: text(finding.evidence, 1536), resolution: text(finding.resolution, 1024) };
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
if (bytes(JSON.stringify(canonical)) > 12288) throw invalid();
|
|
51
|
+
return canonical;
|
|
52
|
+
}
|
|
53
|
+
export function dailyRequest(id, evidence) {
|
|
54
|
+
const request = { id: identifier(id, uuid), consent: true, evidence: canonicalDailyEvidence(evidence) };
|
|
55
|
+
if (bytes(JSON.stringify(request)) > 16384) throw invalid();
|
|
56
|
+
return request;
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Rpc } from '@opencode/plugin/rpc';
|
|
2
|
+
|
|
3
|
+
export type DailyCheck = { name: string; before: 'failed' | 'not_run'; after: 'passed' | 'failed' | 'not_run'; revision: string; result: string };
|
|
4
|
+
export type DailyFinding = { issue: string; evidence: string; resolution: string };
|
|
5
|
+
export type DailyEvidence = { projectId: string; outcomeId: string; runId: string; revision: string; goal: string; change: string; checks: DailyCheck[]; findings: DailyFinding[] };
|
|
6
|
+
export type DailySubmission = {
|
|
7
|
+
id: string; questId: string; status: 'queued' | 'grading' | 'graded' | 'failed'; decision: null | 'assessed_complete' | 'not_met' | 'needs_evidence';
|
|
8
|
+
scores: null | Record<string, number>; model: null | string; rubricVersion: null | string; attempts: number; createdAt: string; consentAt: string;
|
|
9
|
+
finishedAt: null | string; nextAttemptAt: null | string; errorCode: null | 'daily_grader_invalid' | 'daily_grader_unavailable' | 'daily_grader_timeout' | 'daily_grader_aborted' | 'daily_payload_expired' | 'daily_attempts_exhausted';
|
|
10
|
+
assessmentBasis: 'tui_submitted'; rewards: false;
|
|
11
|
+
};
|
|
12
|
+
export type DailyQuest = { id: string; day: string; definition: { templateId: string; title: string; kind: 'change' | 'tests' | 'review'; rubricVersion: string; criteria: { id: string; text: string }[] }; status: 'open' | 'assessed_complete'; endsAt: string; completedAt: string | null; assessmentBasis: 'tui_submitted'; rewards: false };
|
|
13
|
+
export type DailyQuestDetail = DailyQuest & { latestSubmission: DailySubmission | null };
|
|
14
|
+
export type DailySlate = { day: string; endsAt: string; policyVersion: string; items: DailyQuest[]; assessmentBasis: 'tui_submitted'; rewards: false };
|
|
15
|
+
export type DailyRecord = { id: string; questId: string; createdAt: string; request: { id: string; consent: true; evidence: DailyEvidence } | null; submission: DailySubmission | null };
|
|
16
|
+
export type DailySnapshot = { accountKey: string; slate: DailySlate; records: DailyRecord[] };
|
|
17
|
+
|
|
18
|
+
// No arbitrary patterns: pinned native Effect conversion rejects them. Manual
|
|
19
|
+
// validation enforces UUIDs, revisions, UTF-8 limits and cross-field bindings.
|
|
20
|
+
const text = { type: 'string', maxLength: 4096 } as const;
|
|
21
|
+
const id = { type: 'string', minLength: 36, maxLength: 36 } as const;
|
|
22
|
+
const nullableText = { type: ['string', 'null'], maxLength: 4096 } as const;
|
|
23
|
+
const yes = { type: 'boolean', const: true } as const;
|
|
24
|
+
const basis = { assessmentBasis: { type: 'string', const: 'tui_submitted' }, rewards: { type: 'boolean', const: false } } as const;
|
|
25
|
+
const object = <P extends Record<string, unknown>>(properties: P) => ({ type: 'object', properties, required: Object.keys(properties), additionalProperties: false } as const);
|
|
26
|
+
const check = object({ name: text, before: { type: 'string', enum: ['failed', 'not_run'] }, after: { type: 'string', enum: ['passed', 'failed', 'not_run'] }, revision: text, result: text });
|
|
27
|
+
const finding = object({ issue: text, evidence: text, resolution: text });
|
|
28
|
+
const evidence = object({ projectId: id, outcomeId: id, runId: id, revision: text, goal: text, change: text, checks: { type: 'array', maxItems: 12, items: check }, findings: { type: 'array', maxItems: 5, items: finding } });
|
|
29
|
+
const request = object({ id, consent: yes, evidence });
|
|
30
|
+
const submission = object({
|
|
31
|
+
id, questId: id, status: { type: 'string', enum: ['queued', 'grading', 'graded', 'failed'] }, decision: { type: ['string', 'null'], enum: [null, 'assessed_complete', 'not_met', 'needs_evidence'] },
|
|
32
|
+
scores: { type: ['object', 'null'], additionalProperties: { type: 'number', minimum: 0, maximum: 1 } }, model: nullableText, rubricVersion: nullableText,
|
|
33
|
+
attempts: { type: 'integer', minimum: 0, maximum: 4 }, createdAt: text, consentAt: text, finishedAt: nullableText, nextAttemptAt: nullableText,
|
|
34
|
+
errorCode: { type: ['string', 'null'], enum: [null, 'daily_grader_invalid', 'daily_grader_unavailable', 'daily_grader_timeout', 'daily_grader_aborted', 'daily_payload_expired', 'daily_attempts_exhausted'] }, ...basis,
|
|
35
|
+
});
|
|
36
|
+
const questProperties = { id, day: text, definition: object({ templateId: text, title: text, kind: { type: 'string', enum: ['change', 'tests', 'review'] }, rubricVersion: text, criteria: { type: 'array', maxItems: 20, items: object({ id: text, text }) } }), status: { type: 'string', enum: ['open', 'assessed_complete'] }, endsAt: text, completedAt: nullableText, ...basis } as const;
|
|
37
|
+
const quest = object(questProperties);
|
|
38
|
+
const slate = object({ day: text, endsAt: text, policyVersion: text, items: { type: 'array', maxItems: 3, items: quest }, ...basis });
|
|
39
|
+
const record = object({ id, questId: id, createdAt: text, request: { anyOf: [request, { type: 'null' }] }, submission: { anyOf: [submission, { type: 'null' }] } });
|
|
40
|
+
const accountKey = { type: 'string', minLength: 64, maxLength: 64 } as const;
|
|
41
|
+
const errors = { dailyQuestError: object({ code: text, status: { type: 'integer', minimum: 0, maximum: 599 }, uncertain: { type: 'boolean' } }) } as const;
|
|
42
|
+
const result = object({ submission });
|
|
43
|
+
export const DailyQuests = Rpc.define({ id: 'carthagent.dailyQuests', methods: {
|
|
44
|
+
snapshot: { errors, input: { type: 'object', additionalProperties: false }, output: object({ accountKey, slate, records: { type: 'array', maxItems: 64, items: record } }) },
|
|
45
|
+
detail: { errors, input: object({ id }), output: object({ ...questProperties, latestSubmission: { anyOf: [submission, { type: 'null' }] } }) },
|
|
46
|
+
submit: { errors, input: object({ accountKey, questId: id, id, consent: yes, evidence }), output: result },
|
|
47
|
+
retry: { errors, input: object({ accountKey, id }), output: result },
|
|
48
|
+
poll: { errors, input: object({ id }), output: result },
|
|
49
|
+
forgetPayload: { errors, input: object({ accountKey, id }), output: object({ ok: yes }) },
|
|
50
|
+
}, events: {} });
|