carthagent 1.0.7 → 1.1.0-preview.12
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 +80 -79
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +314 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +298 -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/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
- package/plugins/carthagent/src/server/delivery.ts +217 -106
- 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 +58 -30
- 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 +39 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +492 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
- package/plugins/carthagent/src/ui/native-review.mjs +154 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -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 +122 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +127 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +426 -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-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +267 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +75 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +36 -0
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +100 -18
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +83 -61
- 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
|
@@ -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;
|
|
@@ -104,7 +105,7 @@ function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.In
|
|
|
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,15 +113,21 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
112
113
|
openBrowserImpl: typeof openBrowser;
|
|
113
114
|
agentDir: string;
|
|
114
115
|
startDeviceFlow: () => Promise<{ authorization: { userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; deviceCode: string }; callback: Promise<Credential.OAuth> }>;
|
|
115
|
-
rereadCredential: () => Promise<Credential.OAuth | undefined>;
|
|
116
116
|
credentialFile: string;
|
|
117
|
+
coordinationFile: string;
|
|
117
118
|
}) {
|
|
118
119
|
const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl, sleep: ms => sleep(ms) });
|
|
119
120
|
return {
|
|
120
121
|
integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID),
|
|
121
|
-
method: {
|
|
122
|
-
|
|
123
|
-
const legacy
|
|
122
|
+
method: {
|
|
123
|
+
id: OAUTH_METHOD, type: 'oauth' as const, label: 'Sign in with Carthagent Ship',
|
|
124
|
+
form: [{ type: 'boolean' as const, key: 'importLegacy', title: 'Import a legacy Ship credential instead of signing in?',
|
|
125
|
+
description: 'Leave disabled for fresh browser approval and updated account permissions.', default: false }] satisfies Form.Fields,
|
|
126
|
+
},
|
|
127
|
+
authorize: async (answer: Form.Answer) => {
|
|
128
|
+
// The legacy file intentionally survives migration. Never let its presence
|
|
129
|
+
// short-circuit an explicit sign-in or recovery from revoked/old scopes.
|
|
130
|
+
const legacy = answer.importLegacy === true ? await legacyOAuthForOpenCode({ agentDir }) : undefined;
|
|
124
131
|
if (legacy) {
|
|
125
132
|
return {
|
|
126
133
|
mode: 'auto' as const,
|
|
@@ -146,15 +153,21 @@ function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, st
|
|
|
146
153
|
};
|
|
147
154
|
},
|
|
148
155
|
refresh: (credential: Credential.OAuth) => refreshSingleFlight(
|
|
149
|
-
`${credential.methodID}:${credential.refresh}`,
|
|
150
|
-
() =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
`${coordinationFile}:${credential.methodID}:${credential.refresh}`,
|
|
157
|
+
async () => {
|
|
158
|
+
const credentialID = await readNativeOAuthCredential({ credentialFile,
|
|
159
|
+
integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID, refreshToken: credential.refresh });
|
|
160
|
+
return refreshNativeCredential({
|
|
161
|
+
credential,
|
|
162
|
+
credentialFile: coordinationFile,
|
|
163
|
+
credentialID,
|
|
164
|
+
reread: () => readNativeOAuthCredential({ credentialFile, credentialID,
|
|
165
|
+
integrationID: SHIP_INTEGRATION_ID, methodID: credential.methodID }),
|
|
166
|
+
refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
167
|
+
...oauthCredentials(await client.refresh({ refreshToken: current.refresh, signal: AbortSignal.timeout(10_000) }), now()),
|
|
168
|
+
...(current.metadata ? { metadata: current.metadata } : {}) }),
|
|
169
|
+
});
|
|
170
|
+
},
|
|
158
171
|
),
|
|
159
172
|
label: () => 'Carthagent Ship',
|
|
160
173
|
};
|
|
@@ -164,6 +177,7 @@ function integrationDefinitions(editor: Parameters<Context['integration']['trans
|
|
|
164
177
|
editor.method.update(oauthMethod(options));
|
|
165
178
|
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Use an Experiential API key' } });
|
|
166
179
|
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
|
|
180
|
+
editor.update(SHIP_INTEGRATION_ID, integration => { integration.name = EXPERIENTIAL_PROVIDER_NAME; });
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
function asModelInfo(providerID: string, model: { id: string }, baseURL: string) {
|
|
@@ -199,6 +213,7 @@ interface ProviderRegistrationOptions {
|
|
|
199
213
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
200
214
|
agentDir?: string;
|
|
201
215
|
credentialFile?: string;
|
|
216
|
+
vaultFile?: string;
|
|
202
217
|
openBrowser?: typeof openBrowser;
|
|
203
218
|
telemetryIdentity?: ReturnType<typeof createSessionIdentity>;
|
|
204
219
|
sessionIdentity?: (event: SessionModelRequest) => string;
|
|
@@ -215,12 +230,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
215
230
|
const agentDir = options.agentDir ?? shipAgentDir(env);
|
|
216
231
|
const openBrowserImpl = options.openBrowser ?? openBrowser;
|
|
217
232
|
const deviceAuth = options.deviceAuth ?? createDeviceAuthController({ env, fetchImpl, agentDir, now, sleep });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return value?.type === 'oauth' && value.methodID === OAUTH_METHOD ? value : undefined;
|
|
223
|
-
};
|
|
233
|
+
// credentialFile historically selects the coordination namespace. Retain
|
|
234
|
+
// the fixture shorthand (one explicit path for both) unless vaultFile is set.
|
|
235
|
+
const coordinationFile = options.credentialFile ?? nativeCredentialFile(env);
|
|
236
|
+
const credentialFile = options.vaultFile ?? options.credentialFile ?? nativeCredentialDatabaseFile(env, ctx.app.channel);
|
|
224
237
|
const requestIdentity = options.sessionIdentity;
|
|
225
238
|
const identity = options.telemetryIdentity ?? createSessionIdentity();
|
|
226
239
|
const requestSecret = options.requestSecret ?? `${ctx.location?.project?.id || ctx.location?.directory || 'ctg'}:request-v1`;
|
|
@@ -236,23 +249,36 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
236
249
|
return failures;
|
|
237
250
|
};
|
|
238
251
|
try {
|
|
252
|
+
const excludedIntegrations = new Set<string>();
|
|
239
253
|
const integration = await ctx.integration.transform(editor => {
|
|
240
254
|
integrationDefinitions(editor, {
|
|
241
255
|
env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow: deviceAuth.start,
|
|
242
|
-
|
|
243
|
-
|
|
256
|
+
credentialFile,
|
|
257
|
+
coordinationFile,
|
|
244
258
|
});
|
|
259
|
+
for (const integration of editor.list()) {
|
|
260
|
+
const supported = nativeLoginProvider(integration.id);
|
|
261
|
+
if (supported) editor.update(integration.id, item => { item.name = supported.name; });
|
|
262
|
+
else {
|
|
263
|
+
excludedIntegrations.add(integration.id);
|
|
264
|
+
editor.remove(integration.id);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
245
267
|
});
|
|
246
268
|
registrations.push(integration);
|
|
247
269
|
|
|
248
270
|
const baseURL = experientialBaseUrl(env);
|
|
249
|
-
|
|
271
|
+
let shipModels: Model.Info[] = [];
|
|
272
|
+
let shipConnection: ConnectionInfo | undefined;
|
|
250
273
|
const subscriptions: Record<string, Model.Info[]> = { [ANTHROPIC_PROVIDER_ID]: [], [OPENAI_CODEX_PROVIDER_ID]: [] };
|
|
251
274
|
const subscriptionConnections: Partial<Record<string, ConnectionInfo>> = {};
|
|
252
275
|
const providerRegistration = await ctx.provider.transform(editor => {
|
|
276
|
+
for (const { provider } of editor.list()) {
|
|
277
|
+
if (provider.id.startsWith('opencode') || excludedIntegrations.has(provider.integrationID ?? provider.id)) editor.remove(provider.id);
|
|
278
|
+
}
|
|
253
279
|
editor.add({ info: providerInfo(EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
|
|
254
280
|
'@opencode/ai/providers/openai-compatible', SHIP_INTEGRATION_ID,
|
|
255
|
-
{ baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels });
|
|
281
|
+
{ baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels, sourceConnection: shipConnection });
|
|
256
282
|
// Subscription IDs already exist in the native catalog. Overlay only the
|
|
257
283
|
// definitions and retain catalog cost/capability metadata.
|
|
258
284
|
for (const [providerID, baseURL] of [
|
|
@@ -330,8 +356,10 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
330
356
|
const refresh = async (signal?: AbortSignal): Promise<void> => {
|
|
331
357
|
const inventory = await loadShipModels({ ctx, fetchImpl, env, baseURL, signal });
|
|
332
358
|
if (inventory?.models) {
|
|
333
|
-
|
|
334
|
-
|
|
359
|
+
shipConnection = inventory.connection;
|
|
360
|
+
// The host indexes immutable model arrays by identity.
|
|
361
|
+
shipModels = inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
|
|
362
|
+
inventory.managed ? cloudGatewayUrl(env) : baseURL));
|
|
335
363
|
await ctx.provider.reload();
|
|
336
364
|
}
|
|
337
365
|
for (const providerID of [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]) {
|
|
@@ -340,7 +368,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
340
368
|
if (!active) {
|
|
341
369
|
if (subscriptionConnections[providerID]) {
|
|
342
370
|
delete subscriptionConnections[providerID];
|
|
343
|
-
subscriptions[providerID]
|
|
371
|
+
subscriptions[providerID] = [];
|
|
344
372
|
await ctx.provider.reload();
|
|
345
373
|
}
|
|
346
374
|
continue;
|
|
@@ -357,7 +385,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
357
385
|
const accountMetadata = credential && 'metadata' in credential ? credential.metadata as Record<string, unknown> | undefined : undefined;
|
|
358
386
|
const accountKey = createHash('sha256').update(String(('id' in active ? active.id : '') || accountMetadata?.accountId || (credential?.type === 'oauth' ? credential.access : credential?.type === 'key' ? credential.key : '') || '')).digest('hex');
|
|
359
387
|
if (prior?.account !== accountKey) {
|
|
360
|
-
subscriptions[providerID]
|
|
388
|
+
subscriptions[providerID] = [];
|
|
361
389
|
}
|
|
362
390
|
const known = new Map([
|
|
363
391
|
...catalogModels(providerID).map((model: CatalogModel) => [model.id, model] as const),
|
|
@@ -369,7 +397,7 @@ export async function registerProviders(ctx: Context, options: ProviderRegistrat
|
|
|
369
397
|
? anthropicModelDefinition(model, base) : codexModelDefinition(model, base),
|
|
370
398
|
providerID === ANTHROPIC_PROVIDER_ID ? 'https://api.anthropic.com' : 'https://chatgpt.com/backend-api');
|
|
371
399
|
});
|
|
372
|
-
subscriptions[providerID]
|
|
400
|
+
subscriptions[providerID] = converted;
|
|
373
401
|
subscriptionConnections[providerID] = active;
|
|
374
402
|
await ctx.storage.set(cacheKey, { account: accountKey, models: discovered, checkedAt: now() });
|
|
375
403
|
await ctx.provider.reload();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@opencode/plugin/promise/plugin';
|
|
2
|
-
import { framePrompt, shouldFrame, unframe } from '
|
|
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,39 @@
|
|
|
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
|
+
const browserCheck = fileURLToPath(new URL("../../../../tools/browser-check.mjs", import.meta.url))
|
|
12
|
+
const browserGuidance = existsSync(browserCheck)
|
|
13
|
+
? `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).`
|
|
14
|
+
: ""
|
|
15
|
+
|
|
16
|
+
export function originalTask(prompt: string) { return unframe(prompt) ?? prompt }
|
|
17
|
+
|
|
18
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- Host-neutral domain snapshots are JS objects. */
|
|
19
|
+
export function shipGuidance({ state, workflow, prompt = "", profiles = [], discovery, extraGuidance, required = [], imageInput }: {
|
|
20
|
+
state: any; workflow: string; prompt?: string; profiles?: any[]; discovery?: any;
|
|
21
|
+
extraGuidance?: string; required?: any[]; imageInput?: boolean;
|
|
22
|
+
}) {
|
|
23
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
24
|
+
const task = originalTask(prompt)
|
|
25
|
+
const mode = state && !["verified", "blocked"].includes(state.status) ? state.workflowMode : workflow
|
|
26
|
+
const parts = mode === "standard"
|
|
27
|
+
? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE]
|
|
28
|
+
: [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state)]
|
|
29
|
+
if (/\b(test|typescript|route|backend|api)\b/i.test(task)) parts.push(TEST_INFRASTRUCTURE_GUIDANCE)
|
|
30
|
+
if (profiles.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(task)) parts.push(browserGuidance)
|
|
31
|
+
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.")
|
|
32
|
+
if (discovery?.active && mode !== "standard") parts.push(PLANNING_START)
|
|
33
|
+
const routed = formatGuidance(profiles)
|
|
34
|
+
if (routed) parts.push(routed)
|
|
35
|
+
if (extraGuidance) parts.push(`Task-specific delivery context:\n${extraGuidance}`)
|
|
36
|
+
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.`)
|
|
37
|
+
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.')
|
|
38
|
+
return parts.filter(Boolean).join("\n\n")
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CloudClient, cloudControlUrl } from '../../../../lib/cloud/client.mjs';
|
|
2
|
+
import type { Context } from '@opencode/plugin/promise/plugin';
|
|
3
|
+
import type { ConnectionInfo } from '@opencode/client';
|
|
4
|
+
|
|
5
|
+
type ShipContext = Pick<Context, 'integration'>;
|
|
6
|
+
type ActiveCredential = Extract<ConnectionInfo, { type: 'credential' }>;
|
|
7
|
+
type CloudClientPort = Pick<CloudClient, 'account' | 'usage' | 'checkout' | 'portal' | 'revokeCurrent' | 'revokeSession'>;
|
|
8
|
+
|
|
9
|
+
/** Create profile-scoped Ship RPC handlers; injection keeps cloud behavior testable offline. */
|
|
10
|
+
export function createShipRpcHandlers(ctx: ShipContext, options: {
|
|
11
|
+
cloudClient?: CloudClientPort;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
cloudUrl?: string;
|
|
14
|
+
} = {}) {
|
|
15
|
+
const cloud = options.cloudClient ?? new CloudClient({ baseUrl: options.baseUrl ?? cloudControlUrl() });
|
|
16
|
+
async function access() {
|
|
17
|
+
const active = await ctx.integration.connection.active('experiential-labs');
|
|
18
|
+
if (!active || active.type !== 'credential' || active.method !== 'oauth') throw new Error('An active Carthagent Ship OAuth connection is required.');
|
|
19
|
+
const credential = await ctx.integration.connection.resolve(active);
|
|
20
|
+
if (credential?.type !== 'oauth' || credential.methodID !== 'carthagent-device' || !credential.access) {
|
|
21
|
+
throw new Error('An active Carthagent Ship OAuth connection is required.');
|
|
22
|
+
}
|
|
23
|
+
return { active: active as ActiveCredential, token: credential.access };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
shipAccount: async (_input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipAccount>) => {
|
|
27
|
+
call.signal.throwIfAborted();
|
|
28
|
+
let token: string;
|
|
29
|
+
try { ({ token } = await access()); }
|
|
30
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Ship OAuth connection is required.', {}); }
|
|
31
|
+
return { account: await cloud.account({ accessToken: token, signal: call.signal }) };
|
|
32
|
+
},
|
|
33
|
+
shipUsage: async (input: unknown, call: { signal: AbortSignal }) => {
|
|
34
|
+
call.signal.throwIfAborted();
|
|
35
|
+
const { token } = await access();
|
|
36
|
+
const requested = (input as { limit?: number }).limit ?? 50;
|
|
37
|
+
if (!Number.isSafeInteger(requested) || requested < 1 || requested > 200) throw new Error('Ship usage limit must be between 1 and 200.');
|
|
38
|
+
return { usage: await cloud.usage({ accessToken: token, limit: requested, signal: call.signal } as Parameters<CloudClient['usage']>[0]) };
|
|
39
|
+
},
|
|
40
|
+
shipBilling: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipBilling>) => {
|
|
41
|
+
call.signal.throwIfAborted();
|
|
42
|
+
let token: string;
|
|
43
|
+
try { ({ token } = await access()); }
|
|
44
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Ship OAuth connection is required.', {}); }
|
|
45
|
+
const action = (input as { action: 'checkout' | 'portal' }).action;
|
|
46
|
+
if (action !== 'checkout' && action !== 'portal') throw new Error('Unsupported Ship billing action.');
|
|
47
|
+
const returnUrl = `${options.cloudUrl ?? cloudControlUrl()}/account`;
|
|
48
|
+
const result = action === 'portal'
|
|
49
|
+
? await cloud.portal({ accessToken: token, returnUrl, signal: call.signal })
|
|
50
|
+
: await cloud.checkout({ accessToken: token, successUrl: returnUrl, cancelUrl: returnUrl, signal: call.signal });
|
|
51
|
+
return { url: result.url };
|
|
52
|
+
},
|
|
53
|
+
shipLogout: async (input: unknown, call: import('@opencode/plugin/promise/rpc').RpcCallContext<typeof import('../rpc').Carthagent.methods.shipLogout>) => {
|
|
54
|
+
call.signal.throwIfAborted();
|
|
55
|
+
let active: ActiveCredential;
|
|
56
|
+
let token: string;
|
|
57
|
+
try { ({ active, token } = await access()); }
|
|
58
|
+
catch { return call.error('unauthenticated', 'An active Carthagent Ship OAuth connection is required.', {}); }
|
|
59
|
+
const session = (input as { session?: string }).session;
|
|
60
|
+
if (session) {
|
|
61
|
+
await cloud.revokeSession({ accessToken: token, sessionId: session, signal: call.signal });
|
|
62
|
+
return { revoked: true };
|
|
63
|
+
}
|
|
64
|
+
await cloud.revokeCurrent({ accessToken: token, signal: call.signal });
|
|
65
|
+
await ctx.integration.transform((editor) => editor.remove(active.id));
|
|
66
|
+
return { revoked: true, credentialID: active.id };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|