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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behind-the-scenes phased-delivery framing (the carthagent default flow).
|
|
3
|
+
*
|
|
4
|
+
* Every typed task is a phased delivery run: the raw text is what the user
|
|
5
|
+
* sees echoed in the feed, while the engine receives the expanded delivery template
|
|
6
|
+
* body. Resolving the template here (rather than forwarding "/guide <task>"
|
|
7
|
+
* to the engine) keeps the behavior identical in --isolate and headless -p modes,
|
|
8
|
+
* and keeps the machinery invisible in the feed.
|
|
9
|
+
*
|
|
10
|
+
* This is the default flow for carthagent — the whole product is built around it —
|
|
11
|
+
* so the input pill stays empty and the hint rotates through generic
|
|
12
|
+
* builder one-liners.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, dirname, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = resolve(__dirname, '../../../..');
|
|
20
|
+
const TEMPLATE_PATH = join(ROOT, 'prompts', 'delivery.md');
|
|
21
|
+
|
|
22
|
+
let cached = null;
|
|
23
|
+
|
|
24
|
+
export const FRAME_HINTS = ['Message carthagent…'];
|
|
25
|
+
|
|
26
|
+
export function frameHint() {
|
|
27
|
+
return FRAME_HINTS[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTemplate(path) {
|
|
31
|
+
let body = '';
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(path, 'utf8');
|
|
34
|
+
const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
35
|
+
body = (fm ? raw.slice(fm[0].length) : raw).trim();
|
|
36
|
+
} catch { body = ''; }
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Body of prompts/delivery.md with frontmatter stripped. '' when unreadable. */
|
|
41
|
+
export function framingTemplate(path = TEMPLATE_PATH) {
|
|
42
|
+
if (path === TEMPLATE_PATH) {
|
|
43
|
+
if (cached == null) cached = readTemplate(TEMPLATE_PATH);
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
return readTemplate(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Expand the delivery template for a task. Falls back to the bare task text. */
|
|
50
|
+
export function framePrompt(task, template = framingTemplate()) {
|
|
51
|
+
const t = String(task).trim();
|
|
52
|
+
if (!template) return t;
|
|
53
|
+
return template.includes('$@') ? template.replaceAll('$@', t) : `${template}\n\n${t}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Recover the original task text from a framed prompt, else null. */
|
|
57
|
+
export function unframe(text) {
|
|
58
|
+
// Match the current template suffix first: the task itself may contain
|
|
59
|
+
// blank lines (or even phase headings). Keep the legacy format fallback.
|
|
60
|
+
const value = String(text);
|
|
61
|
+
const template = framingTemplate();
|
|
62
|
+
const parts = template.split('$@');
|
|
63
|
+
if (parts.length === 2 && value.startsWith(parts[0]) && value.endsWith(parts[1])) {
|
|
64
|
+
return value.slice(parts[0].length, parts[1] ? -parts[1].length : undefined).trim();
|
|
65
|
+
}
|
|
66
|
+
const m = value.match(/^Deliver this task in phases(?: with executable evidence)?:\s*([\s\S]*?)\n\n/);
|
|
67
|
+
return m ? m[1].trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when a submitted prompt should get the delivery framing. */
|
|
71
|
+
export function shouldFrame(text, { enabled = true, delivery = true } = {}) {
|
|
72
|
+
if (!enabled || !delivery) return false;
|
|
73
|
+
const v = String(text).trim();
|
|
74
|
+
if (!v) return false;
|
|
75
|
+
if (v.startsWith('/') || v.startsWith('!')) return false;
|
|
76
|
+
// Questions/read-only asks are not delivery runs.
|
|
77
|
+
if (v.endsWith('?') && v.length < 300) return false;
|
|
78
|
+
if (/^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) && v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v)) return false;
|
|
79
|
+
return existsSync(TEMPLATE_PATH);
|
|
80
|
+
}
|
|
@@ -8,7 +8,7 @@ type DeliveryState = Parameters<typeof startImplementation>[0]
|
|
|
8
8
|
export const READ_ONLY_TOOLS = new Set(["read", "ls", "find", "grep", "code_nav", "delivery_probe"])
|
|
9
9
|
|
|
10
10
|
export function requiresPlanApproval(tool: string) {
|
|
11
|
-
return needsImplementation(tool)
|
|
11
|
+
return !["glob", "skill", "question"].includes(tool) && needsImplementation(tool.replace(/^carthagent_/, ""))
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function enforceToolPolicy(tool: string, state: DeliveryState, cwd: string) {
|
|
@@ -25,9 +25,10 @@ import {
|
|
|
25
25
|
import { catalogModels } from '../../../../lib/providers/model-catalog.mjs';
|
|
26
26
|
import { shipAgentDir } from '../../../../lib/ship-auth.mjs';
|
|
27
27
|
import { createHash } from 'node:crypto';
|
|
28
|
-
import { legacyOAuthForOpenCode, nativeCredentialFile, refreshNativeCredential, refreshSingleFlight } from '../../../../lib/opencode/credentials.mjs';
|
|
28
|
+
import { legacyOAuthForOpenCode, nativeCredentialFile, nativeCredentialDatabaseFile, readNativeOAuthCredential, refreshNativeCredential, refreshSingleFlight } from '../../../../lib/opencode/credentials.mjs';
|
|
29
29
|
import { createDeviceAuthController } from './device-auth.ts';
|
|
30
30
|
import { createSessionIdentity, isManagedShipCredential, shipTelemetryHeaders } from './telemetry.ts';
|
|
31
|
+
import { nativeLoginProvider } from '../../../../lib/opencode/provider-policy.mjs';
|
|
31
32
|
|
|
32
33
|
const OAUTH_METHOD = 'carthagent-device';
|
|
33
34
|
const SHIP_INTEGRATION_ID = EXPERIENTIAL_PROVIDER_ID;
|
|
@@ -97,14 +98,14 @@ function modelInfo(providerID: string, profile: CatalogModel, defaultBaseURL: st
|
|
|
97
98
|
|
|
98
99
|
function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.Info['compatibility']> {
|
|
99
100
|
const keys = [
|
|
100
|
-
'reasoningField', 'requireReasoning', 'maxTokensField', 'requireFinishReason',
|
|
101
|
+
'reasoningField', 'requireReasoning', 'reasoningPlaceholder', 'maxTokensField', 'requireFinishReason',
|
|
101
102
|
'requireAssistantAfterTool', 'supportsPromptCacheKey', 'cacheFormat',
|
|
102
103
|
'supportsThinkingBlockBinding', 'supportsEffortUpdates',
|
|
103
104
|
] as const;
|
|
104
105
|
return Object.fromEntries(keys.flatMap(key => key in compat ? [[key, compat[key]]] : [])) as NonNullable<Model.Info['compatibility']>;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow,
|
|
108
|
+
function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow, credentialFile, coordinationFile }: {
|
|
108
109
|
env: NodeJS.ProcessEnv;
|
|
109
110
|
fetchImpl: typeof fetch;
|
|
110
111
|
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
@@ -112,20 +113,25 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
112
113
|
openBrowserImpl: typeof openBrowser;
|
|
113
114
|
agentDir: string;
|
|
114
115
|
startDeviceFlow: () => Promise<{ authorization: { userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; deviceCode: string }; callback: Promise<Credential.OAuth> }>;
|
|
115
|
-
rereadCredential: () => Promise<Credential.OAuth | undefined>;
|
|
116
116
|
credentialFile: string;
|
|
117
|
+
coordinationFile: string;
|
|
117
118
|
}) {
|
|
118
119
|
const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl, sleep: ms => sleep(ms) });
|
|
119
120
|
return {
|
|
120
121
|
integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID),
|
|
121
|
-
method: {
|
|
122
|
-
|
|
123
|
-
const legacy
|
|
122
|
+
method: {
|
|
123
|
+
id: OAUTH_METHOD, type: 'oauth' as const, label: 'Continue with Carthagent Cloud',
|
|
124
|
+
...(env.CARTHAGENT_ADVANCED_AUTH === '1' ? { form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Cloud credential?', default: false }] satisfies Form.Fields } : {}),
|
|
125
|
+
},
|
|
126
|
+
authorize: async (answer: Form.Answer) => {
|
|
127
|
+
// The legacy file intentionally survives migration. Never let its presence
|
|
128
|
+
// short-circuit an explicit sign-in or recovery from revoked/old scopes.
|
|
129
|
+
const legacy = env.CARTHAGENT_ADVANCED_AUTH === '1' && answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
|
|
124
130
|
if (legacy) {
|
|
125
131
|
return {
|
|
126
132
|
mode: 'auto' as const,
|
|
127
133
|
url: 'https://api.carthagent.xyz',
|
|
128
|
-
instructions: 'A legacy Carthagent
|
|
134
|
+
instructions: 'A legacy Carthagent Cloud credential was found and will be migrated into native integration storage.',
|
|
129
135
|
callback: Promise.resolve({
|
|
130
136
|
type: 'oauth' as const,
|
|
131
137
|
methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
@@ -140,30 +146,39 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
140
146
|
Promise.resolve(openBrowserImpl(url)).catch(() => false);
|
|
141
147
|
return {
|
|
142
148
|
mode: 'auto' as const, url,
|
|
143
|
-
instructions: `
|
|
149
|
+
instructions: `Sign in and connect this terminal using code ${response.userCode}. Check the account shown in your browser, then return here. If the code expires, start login again.`,
|
|
144
150
|
expiresAt: now() + Number(response.expiresIn || 600) * 1000,
|
|
145
151
|
callback,
|
|
146
152
|
};
|
|
147
153
|
},
|
|
148
154
|
refresh: (credential: Credential.OAuth) => refreshSingleFlight(
|
|
149
|
-
`${credential.methodID}:${credential.refresh}`,
|
|
150
|
-
() =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
`${coordinationFile}:${credential.methodID}:${credential.refresh}`,
|
|
156
|
+
async () => {
|
|
157
|
+
const credentialID = await readNativeOAuthCredential({ credentialFile,
|
|
158
|
+
integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID, refreshToken: credential.refresh });
|
|
159
|
+
return refreshNativeCredential({
|
|
160
|
+
credential,
|
|
161
|
+
credentialFile: coordinationFile,
|
|
162
|
+
credentialID,
|
|
163
|
+
reread: () => readNativeOAuthCredential({ credentialFile, credentialID,
|
|
164
|
+
integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID }),
|
|
165
|
+
refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
166
|
+
...oauthCredentials(await client.refresh({ refreshToken: current.refresh, signal: AbortSignal.timeout(10_000) }), now()),
|
|
167
|
+
...(current.metadata ? { metadata: current.metadata } : {}) }),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
158
170
|
),
|
|
159
|
-
label: () => 'Carthagent
|
|
171
|
+
label: () => 'Carthagent Cloud',
|
|
160
172
|
};
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
function integrationDefinitions(editor: Parameters<Context['integration']['transform']>[0] extends (editor: infer E) => void ? E : never, options: Parameters<typeof oauthMethod>[0]) {
|
|
164
176
|
editor.method.update(oauthMethod(options));
|
|
165
|
-
|
|
177
|
+
if (options.env.CARTHAGENT_ADVANCED_AUTH === '1') {
|
|
178
|
+
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Advanced: use an Experiential API key' } });
|
|
179
|
+
}
|
|
166
180
|
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
|
|
181
|
+
editor.update(SHIP_INTEGRATION_ID, integration => { integration.name = EXPERIENTIAL_PROVIDER_NAME; });
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
function asModelInfo(providerID: string, model: { id: string }, baseURL: string) {
|
|
@@ -183,7 +198,7 @@ async function loadShipModels({ ctx, fetchImpl, env, baseURL, signal }: {
|
|
|
183
198
|
const response = await fetchImpl(`${managed ? cloudGatewayUrl(env) : baseURL}/models`, {
|
|
184
199
|
headers: { authorization: `Bearer ${key}` }, signal: signalWithTimeout(signal, DISCOVERY_TIMEOUT),
|
|
185
200
|
});
|
|
186
|
-
if (!response.ok) throw new Error(`Carthagent
|
|
201
|
+
if (!response.ok) throw new Error(`Carthagent Cloud model discovery failed (${response.status}).`);
|
|
187
202
|
const models = parseExperientialModels(await response.json()).slice(0, PROFILE_CAP);
|
|
188
203
|
if (connection && managed) return { connection, models, managed };
|
|
189
204
|
return { connection, models, managed };
|
|
@@ -199,6 +214,7 @@ interface ProviderRegistrationOptions {
|
|
|
199
214
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
200
215
|
agentDir?: string;
|
|
201
216
|
credentialFile?: string;
|
|
217
|
+
vaultFile?: string;
|
|
202
218
|
openBrowser?: typeof openBrowser;
|
|
203
219
|
telemetryIdentity?: ReturnType<typeof createSessionIdentity>;
|
|
204
220
|
sessionIdentity?: (event: SessionModelRequest) => string;
|
|
@@ -215,12 +231,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
215
231
|
const agentDir = options.agentDir ?? shipAgentDir(env);
|
|
216
232
|
const openBrowserImpl = options.openBrowser ?? openBrowser;
|
|
217
233
|
const deviceAuth = options.deviceAuth ?? createDeviceAuthController({ env, fetchImpl, agentDir, now, sleep });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return value?.type === 'oauth' && value.methodID === OAUTH_METHOD ? value : undefined;
|
|
223
|
-
};
|
|
234
|
+
// credentialFile historically selects the coordination namespace. Retain
|
|
235
|
+
// the fixture shorthand (one explicit path for both) unless vaultFile is set.
|
|
236
|
+
const coordinationFile = options.credentialFile ?? nativeCredentialFile(env);
|
|
237
|
+
const credentialFile = options.vaultFile ?? options.credentialFile ?? nativeCredentialDatabaseFile(env, ctx.app.channel);
|
|
224
238
|
const requestIdentity = options.sessionIdentity;
|
|
225
239
|
const identity = options.telemetryIdentity ?? createSessionIdentity();
|
|
226
240
|
const requestSecret = options.requestSecret ?? `${ctx.location?.project?.id || ctx.location?.directory || 'ctg'}:request-v1`;
|
|
@@ -236,23 +250,36 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
236
250
|
return failures;
|
|
237
251
|
};
|
|
238
252
|
try {
|
|
253
|
+
const excludedIntegrations = new Set<string>();
|
|
239
254
|
const integration = await ctx.integration.transform(editor => {
|
|
240
255
|
integrationDefinitions(editor, {
|
|
241
256
|
env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow: deviceAuth.start,
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
credentialFile,
|
|
258
|
+
coordinationFile,
|
|
244
259
|
});
|
|
260
|
+
for (const integration of editor.list()) {
|
|
261
|
+
const supported = nativeLoginProvider(integration.id);
|
|
262
|
+
if (supported) editor.update(integration.id, item => { item.name = supported.name; });
|
|
263
|
+
else {
|
|
264
|
+
excludedIntegrations.add(integration.id);
|
|
265
|
+
editor.remove(integration.id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
245
268
|
});
|
|
246
269
|
registrations.push(integration);
|
|
247
270
|
|
|
248
271
|
const baseURL = experientialBaseUrl(env);
|
|
249
|
-
|
|
272
|
+
let shipModels: Model.Info[] = [];
|
|
273
|
+
let shipConnection: ConnectionInfo | undefined;
|
|
250
274
|
const subscriptions: Record<string, Model.Info[]> = { [ANTHROPIC_PROVIDER_ID]: [], [OPENAI_CODEX_PROVIDER_ID]: [] };
|
|
251
275
|
const subscriptionConnections: Partial<Record<string, ConnectionInfo>> = {};
|
|
252
276
|
const providerRegistration = await ctx.provider.transform(editor => {
|
|
277
|
+
for (const { provider } of editor.list()) {
|
|
278
|
+
if (provider.id.startsWith('opencode') || excludedIntegrations.has(provider.integrationID ?? provider.id)) editor.remove(provider.id);
|
|
279
|
+
}
|
|
253
280
|
editor.add({ info: providerInfo(EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
|
|
254
281
|
'@opencode/ai/providers/openai-compatible', SHIP_INTEGRATION_ID,
|
|
255
|
-
{ baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels });
|
|
282
|
+
{ baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels, sourceConnection: shipConnection });
|
|
256
283
|
// Subscription IDs already exist in the native catalog. Overlay only the
|
|
257
284
|
// definitions and retain catalog cost/capability metadata.
|
|
258
285
|
for (const [providerID, baseURL] of [
|
|
@@ -330,8 +357,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
330
357
|
const refresh = async (signal?: AbortSignal): Promise<void> => {
|
|
331
358
|
const inventory = await loadShipModels({ ctx, fetchImpl, env, baseURL, signal });
|
|
332
359
|
if (inventory?.models) {
|
|
333
|
-
|
|
334
|
-
|
|
360
|
+
shipConnection = inventory.connection;
|
|
361
|
+
// The host indexes immutable model arrays by identity.
|
|
362
|
+
shipModels = inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
|
|
363
|
+
inventory.managed ? cloudGatewayUrl(env) : baseURL));
|
|
335
364
|
await ctx.provider.reload();
|
|
336
365
|
}
|
|
337
366
|
for (const providerID of [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]) {
|
|
@@ -340,7 +369,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
340
369
|
if (!active) {
|
|
341
370
|
if (subscriptionConnections[providerID]) {
|
|
342
371
|
delete subscriptionConnections[providerID];
|
|
343
|
-
subscriptions[providerID]
|
|
372
|
+
subscriptions[providerID] = [];
|
|
344
373
|
await ctx.provider.reload();
|
|
345
374
|
}
|
|
346
375
|
continue;
|
|
@@ -357,7 +386,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
357
386
|
const accountMetadata = credential && 'metadata' in credential ? credential.metadata as Record<string, unknown> | undefined : undefined;
|
|
358
387
|
const accountKey = createHash('sha256').update(String(('id' in active ? active.id : '') || accountMetadata?.accountId || (credential?.type === 'oauth' ? credential.access : credential?.type === 'key' ? credential.key : '') || '')).digest('hex');
|
|
359
388
|
if (prior?.account !== accountKey) {
|
|
360
|
-
subscriptions[providerID]
|
|
389
|
+
subscriptions[providerID] = [];
|
|
361
390
|
}
|
|
362
391
|
const known = new Map([
|
|
363
392
|
...catalogModels(providerID).map((model: CatalogModel) => [model.id, model] as const),
|
|
@@ -369,7 +398,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
369
398
|
? anthropicModelDefinition(model, base) : codexModelDefinition(model, base),
|
|
370
399
|
providerID === ANTHROPIC_PROVIDER_ID ? 'https://api.anthropic.com' : 'https://chatgpt.com/backend-api');
|
|
371
400
|
});
|
|
372
|
-
subscriptions[providerID]
|
|
401
|
+
subscriptions[providerID] = converted;
|
|
373
402
|
subscriptionConnections[providerID] = active;
|
|
374
403
|
await ctx.storage.set(cacheKey, { account: accountKey, models: discovered, checkedAt: now() });
|
|
375
404
|
await ctx.provider.reload();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@opencode/plugin/promise/plugin';
|
|
2
|
-
import { framePrompt, shouldFrame, unframe } from '
|
|
2
|
+
import { framePrompt, shouldFrame, unframe } from './framing.mjs';
|
|
3
3
|
import { SessionControls } from '../session-rpc';
|
|
4
|
+
import { isShipAgent } from './ship-agents';
|
|
4
5
|
import { effectiveSessionOptions } from '../../../../lib/opencode/session-options.mjs';
|
|
5
6
|
|
|
6
7
|
/** Admission framing is session-scoped; raw bypasses framing, never delivery policy. */
|
|
@@ -25,6 +26,7 @@ export async function registerSessionControls(ctx: Context) {
|
|
|
25
26
|
const prompt = await ctx.session.hook('prompt', async (event) => {
|
|
26
27
|
const session = await ctx.session.get({ sessionID: event.sessionID });
|
|
27
28
|
const options = effectiveSessionOptions(session, ctx.options);
|
|
29
|
+
if (!isShipAgent(session.agent)) return;
|
|
28
30
|
const saved = await settings(event.sessionID);
|
|
29
31
|
if (event.metadata?.['ctg.raw'] === true || event.metadata?.['ctg.framed'] === true) return;
|
|
30
32
|
if (!shouldFrame(event.prompt.text, {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/promise/plugin"
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
export const SHIP_AGENT_ID = "ship"
|
|
7
|
+
export const LITE_AGENT_ID = "lite"
|
|
8
|
+
|
|
9
|
+
const SKILLS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../skills")
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The delivery harness runs only for Ship. A missing agent ID means the host could not
|
|
13
|
+
* identify the mode, so enforcement stays on rather than silently disappearing.
|
|
14
|
+
*/
|
|
15
|
+
export function isShipAgent(agent: string | undefined | null) {
|
|
16
|
+
return agent === undefined || agent === null || agent === "" || agent === SHIP_AGENT_ID
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type AgentEditor = Parameters<Parameters<Context["agent"]["transform"]>[0]>[0]
|
|
20
|
+
type SkillEditor = Parameters<Parameters<Context["skill"]["transform"]>[0]>[0]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ship deliberately sets no `system` override: the host keeps its native base prompt and the
|
|
24
|
+
* delivery plugin injects the workflow per request, as the legacy extension appended it.
|
|
25
|
+
*/
|
|
26
|
+
export async function registerShipAgents(ctx: Pick<Context, "agent">) {
|
|
27
|
+
const registration = await ctx.agent.transform((editor: AgentEditor) => {
|
|
28
|
+
editor.update(SHIP_AGENT_ID, item => {
|
|
29
|
+
item.name = "Ship" as unknown as typeof item.name
|
|
30
|
+
item.description = "Carthagent delivery workflow: acceptance contract, implementation, fresh check evidence, review and verified handoff."
|
|
31
|
+
item.mode = "primary"
|
|
32
|
+
item.system = undefined
|
|
33
|
+
item.color = "#e6c480"
|
|
34
|
+
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
|
|
35
|
+
})
|
|
36
|
+
editor.update(LITE_AGENT_ID, item => {
|
|
37
|
+
item.name = "Lite" as unknown as typeof item.name
|
|
38
|
+
item.description = "Direct execution with existing skills and tools. No delivery harness."
|
|
39
|
+
item.mode = "primary"
|
|
40
|
+
item.color = "#b89ce0"
|
|
41
|
+
item.system = "You are a direct execution agent. Use the skills and tools already available and work the task directly. No extra planning ceremony, no harness phases — just do the work well and concisely."
|
|
42
|
+
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
|
|
43
|
+
})
|
|
44
|
+
editor.default(SHIP_AGENT_ID)
|
|
45
|
+
})
|
|
46
|
+
return async () => { await registration.dispose() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function frontmatter(raw: string) {
|
|
50
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
51
|
+
if (!match) return { fields: {} as Record<string, string>, body: raw }
|
|
52
|
+
const fields: Record<string, string> = {}
|
|
53
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
54
|
+
const field = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/)
|
|
55
|
+
if (field) fields[field[1]] = field[2].trim().replace(/^["']|["']$/g, "")
|
|
56
|
+
}
|
|
57
|
+
return { fields, body: raw.slice(match[0].length) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function bundledSkills(root = SKILLS_ROOT) {
|
|
61
|
+
if (!existsSync(root)) return []
|
|
62
|
+
return readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory()).flatMap(entry => {
|
|
63
|
+
const path = join(root, entry.name, "SKILL.md")
|
|
64
|
+
if (!existsSync(path)) return []
|
|
65
|
+
const { fields, body } = frontmatter(readFileSync(path, "utf8"))
|
|
66
|
+
return [{ id: entry.name, name: fields.name || entry.name, ...(fields.description ? { description: fields.description } : {}), path, content: body }]
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Legacy runs passed `--skill skills/`; the native host receives the same skill set. */
|
|
71
|
+
export async function registerShipSkills(ctx: Pick<Context, "skill">, root = SKILLS_ROOT) {
|
|
72
|
+
const skills = bundledSkills(root)
|
|
73
|
+
const registration = await ctx.skill.transform((editor: SkillEditor) => {
|
|
74
|
+
for (const skill of skills) if (!editor.get(skill.id)) editor.add(skill as Parameters<SkillEditor["add"]>[0])
|
|
75
|
+
})
|
|
76
|
+
return async () => { await registration.dispose() }
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { archiveContext } from '../../../../lib/context-archive.mjs';
|
|
2
|
+
|
|
3
|
+
const DELIVERY = new Set(['delivery_plan', 'delivery_revise', 'delivery_design', 'delivery_status', 'delivery_progress', 'delivery_check', 'delivery_review']);
|
|
4
|
+
export const localToolName = name => String(name).replace(/^carthagent[._]/, '');
|
|
5
|
+
const blocking = value => /"severity"\s*:\s*"blocking"|"passed"\s*:\s*false/.test(JSON.stringify(value).replaceAll('\\"', '"'));
|
|
6
|
+
|
|
7
|
+
// Native results can group multiple calls in one message. Never split a call/result
|
|
8
|
+
// pair or discard other content on the assistant/result messages when archiving.
|
|
9
|
+
export function archiveDeliveryExchanges(messages, { cwd, directory, allow = false, keepRecent = 2 }) {
|
|
10
|
+
if (!allow) return { messages, failed: false };
|
|
11
|
+
const groups = [];
|
|
12
|
+
for (let i = 0; i < messages.length; i++) {
|
|
13
|
+
const message = messages[i];
|
|
14
|
+
if (message.role !== 'assistant' || !message.content?.length || message.content.some(part => part.type !== 'tool-call' || !DELIVERY.has(localToolName(part.name)))) continue;
|
|
15
|
+
const ids = new Set(message.content.map(part => part.id));
|
|
16
|
+
if (ids.has(undefined) || ids.size !== message.content.length) continue;
|
|
17
|
+
let end = i + 1;
|
|
18
|
+
const results = [];
|
|
19
|
+
while (end < messages.length && messages[end].role === 'tool') {
|
|
20
|
+
results.push(...messages[end].content);
|
|
21
|
+
end++;
|
|
22
|
+
if (results.length >= ids.size) break;
|
|
23
|
+
}
|
|
24
|
+
if (results.length !== ids.size || results.some(part => part.type !== 'tool-result' || !ids.has(part.id) || part.result?.type === 'error' || blocking(part.result))) continue;
|
|
25
|
+
if (new Set(results.map(part => part.id)).size !== ids.size) continue;
|
|
26
|
+
groups.push({ start: i, end, tools: message.content.map(part => part.name) });
|
|
27
|
+
i = end - 1;
|
|
28
|
+
}
|
|
29
|
+
const replace = new Map(); let failed = false;
|
|
30
|
+
for (const group of groups.slice(0, Math.max(0, groups.length - keepRecent))) {
|
|
31
|
+
try {
|
|
32
|
+
const archive = archiveContext(cwd, directory, messages.slice(group.start, group.end));
|
|
33
|
+
replace.set(group.start, { ...group, archive });
|
|
34
|
+
} catch { failed = true; }
|
|
35
|
+
}
|
|
36
|
+
const projected = [];
|
|
37
|
+
for (let i = 0; i < messages.length;) {
|
|
38
|
+
const group = replace.get(i);
|
|
39
|
+
if (!group) { projected.push(messages[i++]); continue; }
|
|
40
|
+
projected.push({ role: 'user', content: [{ type: 'text', text: `Archived successful delivery exchange (not proof of correctness). Tools: ${group.tools.join(', ')}. Read ${group.archive.path} for original messages; sha256=${group.archive.digest}. Current obligations/failures are in the delivery recovery record.` }] });
|
|
41
|
+
i = group.end;
|
|
42
|
+
}
|
|
43
|
+
return { messages: projected, failed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeDesignReview(input) {
|
|
47
|
+
const review = input.review ?? { challenges: input.challenges, walkthroughs: input.walkthroughs, findings: input.findings, limitations: input.limitations };
|
|
48
|
+
return { ...review, captureId: review.captureId ?? input.captureId ?? review.id };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function shellProtection(command, initialFiles) {
|
|
52
|
+
const broad = /\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)|\btaskkill\b[^\n]*(?:\/im|\*)|\bstop-process\b[^\n]*\s-name\b|\bget-process\b[^\n]*\|\s*stop-process\b/i;
|
|
53
|
+
if (broad.test(command)) return 'Broad process termination is blocked. Stop only the specific PID/process you started.';
|
|
54
|
+
for (const file of initialFiles) {
|
|
55
|
+
const escaped = file.split(/[\\/]/).at(-1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
56
|
+
if (new RegExp(`(?:>|tee\\s+)\\s*["']?[^\\s;|]*${escaped}(?:["'\\s;|]|$)`).test(command)) return 'Shell redirection cannot overwrite an existing source file. Use a focused edit.';
|
|
57
|
+
if (/writeFile|write_text|write_bytes/.test(command) && command.includes(file.split(/[\\/]/).at(-1))) return 'A script cannot replace an existing source file. Use a focused edit.';
|
|
58
|
+
if (new RegExp(`(?:^|[;&|]\\s*)(?:rm|mv|cp)\\s+[^;|]*${escaped}(?:["'\\s;|]|$)`).test(command)) return 'Shell commands cannot remove or replace an existing source file. Use a focused edit.';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { DELIVERY_INVARIANTS, TEST_INFRASTRUCTURE_GUIDANCE, deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
|
|
4
|
+
import { PLANNING_START } from "../../../../lib/planning-guide.mjs"
|
|
5
|
+
import { formatGuidance } from "../../../../lib/guidance.mjs"
|
|
6
|
+
import { looksInformational } from "../../../../lib/delivery.mjs"
|
|
7
|
+
import { unframe } from "./framing.mjs"
|
|
8
|
+
|
|
9
|
+
export const STANDARD_GUIDANCE = "Standard delivery: inspect relevant files (glob/read/code_nav); create delivery_plan with complete requirements, artifact roots, a few steps and real checks. A valid required plan enables implementation immediately: design components/scenario ceremony is optional. Revisions merge design/workflow fields, replace supplied arrays, preserve obligations and require fresh final evidence, not repeated approval. Mark implementation steps done independently of integrated check execution. Use delivery_check all, final delivery_review inspect/record, then delivery_finish with honest limitations. No fabricated evidence, scope reduction or unrequested dependency changes. Informational plans do not authorize implementation. For bounded environment versions use delivery_probe kind=node-version or npm-version; inspect other APIs through read."
|
|
10
|
+
|
|
11
|
+
export const TOOL_SURFACE_GUIDANCE = "Read files with direct read/glob/grep tools. Ship withholds direct write/edit/shell tools until implementation is authorized: first create delivery_plan with a valid required acceptance contract; standard mode then unlocks implementation on the next tool turn, while strict mode also requires tested-design approval. File edits and commands use those direct tools, including shell (not bash), outside execute. The code-mode search() catalog lists only namespaced tools.* helpers, not direct tools. If write/edit/shell are missing, complete the required delivery step instead of searching catalogs or runtime internals for alternatives. Use delivery_probe for bounded pre-plan version discovery; never use delivery_check to bootstrap file edits."
|
|
12
|
+
|
|
13
|
+
const browserCheck = fileURLToPath(new URL("../../../../tools/browser-check.mjs", import.meta.url))
|
|
14
|
+
const browserGuidance = existsSync(browserCheck)
|
|
15
|
+
? `For web/browser tasks, a self-contained browser check is available: ${JSON.stringify(browserCheck)}. Declare it as a runtime check, e.g. ["node",${JSON.stringify(browserCheck)},"--page","index.html","--assert","#app","--assert-count","#items:3","--click","#btn","--then-text","#out:done","--console-clean","--screenshot","artifacts/ui.png"]. It serves the workspace over HTTP, runs jsdom assertions with real inline-script execution (clicks, text, selectors, console-error detection), and captures a real screenshot with the bundled lightweight headless browser (auto-downloaded on first use).`
|
|
16
|
+
: ""
|
|
17
|
+
|
|
18
|
+
export function originalTask(prompt: string) { return unframe(prompt) ?? prompt }
|
|
19
|
+
|
|
20
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- Host-neutral domain snapshots are JS objects. */
|
|
21
|
+
export function shipGuidance({ state, workflow, prompt = "", profiles = [], discovery, extraGuidance, required = [], imageInput }: {
|
|
22
|
+
state: any; workflow: string; prompt?: string; profiles?: any[]; discovery?: any;
|
|
23
|
+
extraGuidance?: string; required?: any[]; imageInput?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
26
|
+
const task = originalTask(prompt)
|
|
27
|
+
const mode = state && !["verified", "blocked"].includes(state.status) ? state.workflowMode : workflow
|
|
28
|
+
const parts = mode === "standard"
|
|
29
|
+
? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE, TOOL_SURFACE_GUIDANCE]
|
|
30
|
+
: [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state), TOOL_SURFACE_GUIDANCE]
|
|
31
|
+
if (/\b(test|typescript|route|backend|api)\b/i.test(task)) parts.push(TEST_INFRASTRUCTURE_GUIDANCE)
|
|
32
|
+
if (profiles.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(task)) parts.push(browserGuidance)
|
|
33
|
+
if (imageInput === false) parts.push("This session's model cannot process images: do not attempt to view, read, or reason over image files or screenshots — visual review is unavailable. Verify visual behavior through DOM structure, computed styles, and console evidence instead; screenshots remain useful as evidence artifacts for the user even though you cannot inspect them.")
|
|
34
|
+
if (discovery?.active && mode !== "standard") parts.push(PLANNING_START)
|
|
35
|
+
const routed = formatGuidance(profiles)
|
|
36
|
+
if (routed) parts.push(routed)
|
|
37
|
+
if (extraGuidance) parts.push(`Task-specific delivery context:\n${extraGuidance}`)
|
|
38
|
+
if (required.length) parts.push(`User-owned required validators will be added to your plan automatically: ${JSON.stringify(required)}. Run delivery_check id="all"; repair failures rather than replacing or bypassing these checks.`)
|
|
39
|
+
if (looksInformational(task)) parts.push('Receipt check: this prompt reads as informational/read-only. Answer it directly, or if you record a contract use delivery_plan verification:"none" (no checks) or "advisory" (optional non-blocking evidence). Do not enter the verify/repair loop for a pure question — decide the classification now, before the verify loop.')
|
|
40
|
+
return parts.filter(Boolean).join("\n\n")
|
|
41
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CloudClient, cloudControlUrl } from '../../../../lib/cloud/client.mjs';
|
|
2
|
+
import type { Context } from '@opencode/plugin/promise/plugin';
|
|
3
|
+
import type { ConnectionInfo } from '@opencode/client';
|
|
4
|
+
|
|
5
|
+
type ShipContext = Pick<Context, 'integration'>;
|
|
6
|
+
type ActiveCredential = Extract<ConnectionInfo, { type: 'credential' }>;
|
|
7
|
+
type CloudClientPort = Pick<CloudClient, 'account' | 'usage' | 'quote' | 'checkout' | 'portal' | 'revokeCurrent' | 'revokeSession'>;
|
|
8
|
+
|
|
9
|
+
/** Create profile-scoped Ship RPC handlers; injection keeps cloud behavior testable offline. */
|
|
10
|
+
export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
11
|
+
cloudClient?: CloudClientPort;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
cloudUrl?: string;
|
|
14
|
+
} = {}) {
|
|
15
|
+
const cloud = options.cloudClient ?? new CloudClient({ baseUrl: options.baseUrl ?? cloudControlUrl() });
|
|
16
|
+
async function access() {
|
|
17
|
+
const active = await ctx.integration.connection.active('experiential-labs');
|
|
18
|
+
if (!active || active.type !== 'credential' || active.method !== 'oauth') throw new Error('An active Carthagent Cloud OAuth connection is required.');
|
|
19
|
+
const credential = await ctx.integration.connection.resolve(active);
|
|
20
|
+
if (credential?.type !== 'oauth' || credential.methodID !== 'carthagent-device' || !credential.access) {
|
|
21
|
+
throw new Error('An active Carthagent Cloud OAuth connection is required.');
|
|
22
|
+
}
|
|
23
|
+
return { active: active as ActiveCredential, token: credential.access };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
shipAccount: async (_input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipAccount>) => {
|
|
27
|
+
call.signal.throwIfAborted();
|
|
28
|
+
let token: string;
|
|
29
|
+
try { ({ token } = await access()); }
|
|
30
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
31
|
+
return { account: await cloud.account({ accessToken: token, signal: call.signal }) };
|
|
32
|
+
},
|
|
33
|
+
shipUsage: async (input: unknown, call: { signal: AbortSignal }) => {
|
|
34
|
+
call.signal.throwIfAborted();
|
|
35
|
+
const { token } = await access();
|
|
36
|
+
const requested = (input as { limit?: number }).limit ?? 50;
|
|
37
|
+
if (!Number.isSafeInteger(requested) || requested < 1 || requested > 200) throw new Error('Cloud usage limit must be between 1 and 200.');
|
|
38
|
+
return { usage: await cloud.usage({ accessToken: token, limit: requested, signal: call.signal } as Parameters<CloudClient['usage']>[0]) };
|
|
39
|
+
},
|
|
40
|
+
shipBilling: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipBilling>) => {
|
|
41
|
+
call.signal.throwIfAborted();
|
|
42
|
+
let token: string;
|
|
43
|
+
try { ({ token } = await access()); }
|
|
44
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
45
|
+
const { action, plan = 'builder' } = input as { action: 'checkout' | 'portal' | 'quote'; plan?: string };
|
|
46
|
+
if (!['checkout', 'portal', 'quote'].includes(action)) throw new Error('Unsupported Cloud billing action.');
|
|
47
|
+
if (!['builder', 'pro', 'max'].includes(plan)) throw new Error('Choose Builder, Pro, or Max.');
|
|
48
|
+
if (action === 'quote') return { quote: await cloud.quote({ accessToken: token, plan, signal: call.signal }) };
|
|
49
|
+
const returnUrl = `${options.cloudUrl ?? cloudControlUrl()}/account`;
|
|
50
|
+
const result = action === 'portal'
|
|
51
|
+
? await cloud.portal({ accessToken: token, returnUrl, signal: call.signal })
|
|
52
|
+
: await cloud.checkout({ accessToken: token, priceId: plan, successUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/complete`, cancelUrl: `${options.cloudUrl ?? cloudControlUrl()}/v1/browser/billing/cancelled?plan=${plan}`, signal: call.signal });
|
|
53
|
+
return { url: result.url };
|
|
54
|
+
},
|
|
55
|
+
shipLogout: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipLogout>) => {
|
|
56
|
+
call.signal.throwIfAborted();
|
|
57
|
+
let active: ActiveCredential;
|
|
58
|
+
let token: string;
|
|
59
|
+
try { ({ active, token } = await access()); }
|
|
60
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Cloud OAuth connection is required.', {}); }
|
|
61
|
+
const session = (input as { session?: string }).session;
|
|
62
|
+
if (session) {
|
|
63
|
+
await cloud.revokeSession({ accessToken: token, sessionId: session, signal: call.signal });
|
|
64
|
+
return { revoked: true };
|
|
65
|
+
}
|
|
66
|
+
await cloud.revokeCurrent({ accessToken: token, signal: call.signal });
|
|
67
|
+
await ctx.integration.transform((editor) => editor.remove(active.id));
|
|
68
|
+
return { revoked: true, credentialID: active.id };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|