carthagent 1.1.0-beta.2 → 1.1.0-beta.4
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/docs/beta2-published-acceptance.md +79 -0
- package/docs/beta3-published-acceptance.md +75 -0
- package/docs/cloud-fresh-install-acceptance.md +135 -0
- package/docs/cloud-onboarding-acceptance.md +6 -3
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch +14046 -0
- package/docs/release-notes-beta.3.md +47 -0
- package/docs/release-notes-beta.4.md +18 -0
- package/lib/opencode/cli-runtime.mjs +34 -4
- package/lib/opencode/cli.mjs +6 -3
- package/lib/opencode/runtime-release.json +12 -12
- package/lib/opencode/runtime.mjs +2 -2
- package/package.json +1 -1
- package/plugins/carthagent/src/tui.tsx +2 -2
- package/script/ctg/build-owned-runtime.sh +3 -3
- package/tests/cli.test.mjs +2 -2
- package/tests/native-ship-plan.test.mjs +9 -4
- package/tests/opencode-cli.test.mjs +69 -5
- package/tests/owned-runtime-publish-workflow.test.mjs +12 -6
- package/tests/runtime-development-trust.test.mjs +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Carthagent 1.1.0-beta.3 — release candidate
|
|
2
|
+
|
|
3
|
+
This candidate makes continuation failures actionable. Publication is pending;
|
|
4
|
+
these notes do not establish npm availability.
|
|
5
|
+
|
|
6
|
+
## Continue the existing task
|
|
7
|
+
|
|
8
|
+
- An exhausted persistent execution budget now reports the reason and recovery
|
|
9
|
+
command instead of an unexplained interrupt.
|
|
10
|
+
- `--reset-budget` explicitly authorizes a fresh budget for a resumed session:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
ctg --print --continue --reset-budget "Continue the task"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Ordinary continuation and payment preserve the execution budget, including
|
|
17
|
+
elapsed idle time. Billing credit is separate. Interactive users can run
|
|
18
|
+
`/delivery-budget-reset` before continuing the existing task.
|
|
19
|
+
- CLI help consistently names Carthagent Cloud and documents `billing refresh`.
|
|
20
|
+
|
|
21
|
+
## Acceptance evidence
|
|
22
|
+
|
|
23
|
+
The source fix passed 1,132 tests with five skips, builds and lint. PR #242 CI
|
|
24
|
+
passed Linux, macOS and Windows, browser and installed-package gates. One macOS
|
|
25
|
+
22 service-stop identity timeout passed on retry; the separate push run passed.
|
|
26
|
+
|
|
27
|
+
The authorized existing Cloud account completed a real range-fix task. The same
|
|
28
|
+
session, profile and installed beta.2 runtime then continued with the source
|
|
29
|
+
launcher and explicit reset, adding a regression and passing all five project
|
|
30
|
+
tests independently. Final beta.3 installed acceptance remains required.
|
|
31
|
+
|
|
32
|
+
Backend PRs #180–181 are deployed: automatic payment-status checks, clearer
|
|
33
|
+
sign-in and refund guidance, direct Builder review, modern Stripe compatibility
|
|
34
|
+
and accurate scheduled-cancellation dates while retaining paid access.
|
|
35
|
+
Sandbox purchases, prorated upgrades, initial and upgrade SCA, declined-upgrade
|
|
36
|
+
recovery, renewal, failed renewal, automatic refunds and disputes were exercised
|
|
37
|
+
separately. These do not establish a single
|
|
38
|
+
useful-task → payment boundary → same-task continuation journey: the model task
|
|
39
|
+
used existing production credit and payments used an isolated sandbox backend.
|
|
40
|
+
|
|
41
|
+
## Release channel
|
|
42
|
+
|
|
43
|
+
Targets npm `next`; stable `latest` remains 1.0.7. Runtime prebuild
|
|
44
|
+
[41537249365](https://github.com/B-icy/Carthagent/actions/runs/41537249365) uses
|
|
45
|
+
reviewed source `c11103f8b57c6b3254473a78d535a404bea9a84a` and the unchanged
|
|
46
|
+
combined-v5 runtime recipe. Final assets and manifest must pass immutable draft
|
|
47
|
+
acceptance before publication.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Carthagent 1.1.0-beta.4
|
|
2
|
+
|
|
3
|
+
## Cloud-first onboarding
|
|
4
|
+
|
|
5
|
+
- Fresh home shows **Continue with Carthagent Cloud** and explains sign-in and eligible starter credit.
|
|
6
|
+
- `/login` opens Cloud authorization directly. `/connect` opens the provider picker with Cloud first under Recommended.
|
|
7
|
+
- Terminal `ctg login` accepts Enter to choose its recommended Cloud default.
|
|
8
|
+
|
|
9
|
+
## Continuing useful tasks
|
|
10
|
+
|
|
11
|
+
- An explicit model selection when resuming a headless session now changes that session's model before continuing.
|
|
12
|
+
- Headless observation allows at least 30 minutes instead of interrupting tasks after two minutes. Persistent execution budgets still apply, and `--reset-budget` remains an explicit authorization.
|
|
13
|
+
|
|
14
|
+
## Validation scope
|
|
15
|
+
|
|
16
|
+
The onboarding candidate was exercised in real narrow and wide terminals, with email authorization and actual model calls. A real Cloud ledger and Stripe test-mode payment verified quota rejection, payment activation, and explicit paid continuation of the same session. Payment alone did not restart the task or modify its files.
|
|
17
|
+
|
|
18
|
+
Independent review rejected the generated expense-reporting task as unfinished and incorrect; this release does not claim successful completion of that task or production inference acceptance. See [the acceptance record](cloud-fresh-install-acceptance.md) for details and the local-profile backup incident.
|
|
@@ -100,6 +100,7 @@ function errorText(error) {
|
|
|
100
100
|
function headlessFailureMessage(latest, headlessError) {
|
|
101
101
|
const raw = errorText(latest?.error ?? latest?.session?.error ?? lastAssistantError(latest?.messages) ?? headlessError);
|
|
102
102
|
const outcome = latest?.outcome ?? latest?.session?.outcome;
|
|
103
|
+
if (['elapsed-time', 'tool-calls', 'repair-rounds'].includes(latest?.budgetReason)) return `Session execution budget exhausted: ${latest.budgetReason}. Billing refresh does not reset execution limits. To authorize a fresh budget, resume this session with --reset-budget (for example: ctg --print --continue --reset-budget "Continue the task"), or run /delivery-budget-reset in the existing terminal session before continuing.`;
|
|
103
104
|
if (/ModelNotSelectedError|No model is available|no provider|provider.*not (connected|selected|configured)|not authenticated/i.test(raw ?? '')) {
|
|
104
105
|
return 'No AI provider connected. Run `ctg login` to connect one (Carthagent Cloud is recommended), or set an API key env var (e.g. ANTHROPIC_API_KEY), then retry. Run `ctg native --list-models` to see available models.';
|
|
105
106
|
}
|
|
@@ -559,6 +560,9 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
559
560
|
options.session = await latestSessionForWorkspace(client, cwd);
|
|
560
561
|
if (!options.session) throw new Error('No native Carthagent session exists for this workspace to continue.');
|
|
561
562
|
}
|
|
563
|
+
if (options.resetBudget && !options.session && !options.continue && !options.resume) {
|
|
564
|
+
throw new Error('--reset-budget requires --session, --continue or --resume.');
|
|
565
|
+
}
|
|
562
566
|
eventController = new AbortController();
|
|
563
567
|
const nativeEvents = [];
|
|
564
568
|
let eventFailure;
|
|
@@ -600,6 +604,9 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
600
604
|
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
601
605
|
await client.rpc(definitions.SessionControls).framing({ sessionID, enabled: options.autoFraming }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
602
606
|
}
|
|
607
|
+
if (options.resetBudget && sessionID) {
|
|
608
|
+
await client.session.command({ sessionID, name: 'delivery-budget-reset', text: '' }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
609
|
+
}
|
|
603
610
|
if (framedPrompt) {
|
|
604
611
|
if (!sessionID) {
|
|
605
612
|
const created = await headlessApi.createSession(service, cwd, { body: {
|
|
@@ -637,6 +644,11 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
637
644
|
let result;
|
|
638
645
|
let headlessError;
|
|
639
646
|
const requestedSession = options.session;
|
|
647
|
+
if (requestedSession && explicitModelSelection && model && !signal.controller.signal.aborted) {
|
|
648
|
+
const resumed = await client.session.get({ sessionID: requestedSession }, { signal: signal.controller.signal });
|
|
649
|
+
assertSessionWorkspace(resumed, cwd);
|
|
650
|
+
await client.session.switchModel({ sessionID: requestedSession, model }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
651
|
+
}
|
|
640
652
|
if (requestedSession && Object.keys(sessionSettings).length && !signal.controller.signal.aborted) {
|
|
641
653
|
await persistSessionOptions(client, requestedSession, metadata, sessionSettings, { signal: signal.controller.signal, directory: cwd });
|
|
642
654
|
}
|
|
@@ -644,8 +656,16 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
644
656
|
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
645
657
|
await client.rpc(definitions.SessionControls).framing({ sessionID: requestedSession, enabled: options.autoFraming }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
646
658
|
}
|
|
659
|
+
if (requestedSession && options.resetBudget && !signal.controller.signal.aborted) {
|
|
660
|
+
const resumed = await client.session.get({ sessionID: requestedSession }, { signal: signal.controller.signal });
|
|
661
|
+
assertSessionWorkspace(resumed, cwd);
|
|
662
|
+
await client.session.command({ sessionID: requestedSession, name: 'delivery-budget-reset', text: '' }, { location: { directory: cwd }, signal: signal.controller.signal });
|
|
663
|
+
}
|
|
647
664
|
try {
|
|
648
665
|
result = await headlessApi.runHeadless(service, cwd, framedPrompt || prompt || '', {
|
|
666
|
+
// The transport observer must allow useful coding tasks to finish.
|
|
667
|
+
// Persistent execution budgets remain enforced by the host plugin.
|
|
668
|
+
timeout: Math.max(30 * 60_000, (Number(sessionSettings.maxSeconds) || 0) * 1000 + 5000),
|
|
649
669
|
sessionID: options.session,
|
|
650
670
|
session: {
|
|
651
671
|
title: promptText.slice(0, 80), ...(model ? { model } : {}), metadata,
|
|
@@ -658,7 +678,16 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
658
678
|
headlessError = error;
|
|
659
679
|
result = { session: { id: options.session }, messages: [], outcome: signal.controller.signal.aborted ? 'interrupted' : 'failed', terminal: true, error: error.message };
|
|
660
680
|
}
|
|
661
|
-
|
|
681
|
+
let latest = headlessError ? result : await reconcileHeadlessResult(result, { client, signal: signal.controller.signal });
|
|
682
|
+
if (latest.outcome === 'interrupted' && !latest.error && !latest.session?.error && !lastAssistantError(latest.messages) && latest.session?.id && typeof client.rpc === 'function' && !signal.controller.signal.aborted) {
|
|
683
|
+
// An interrupt may carry no error in the host projection. Read the
|
|
684
|
+
// authoritative persistent budget without restarting the task.
|
|
685
|
+
try {
|
|
686
|
+
const definitions = native.rpcDefinitions ?? await nativeRpcDefinitions();
|
|
687
|
+
const budget = await client.rpc(definitions.Carthagent).budgetStatus({ sessionID: latest.session.id }, { location: { directory: cwd }, signal: AbortSignal.any([signal.controller.signal, AbortSignal.timeout(2000)]) });
|
|
688
|
+
if (budget?.stopped) latest = { ...latest, budgetReason: budget.stopped, error: latest.error ?? headlessFailureMessage({ budgetReason: budget.stopped }) };
|
|
689
|
+
} catch { /* Keep the original failure when budget status is unavailable. */ }
|
|
690
|
+
}
|
|
662
691
|
eventController.abort();
|
|
663
692
|
await eventReader;
|
|
664
693
|
if (eventFailure) throw new Error(`Native Carthagent event stream failed: ${eventFailure.message}`, { cause: eventFailure });
|
|
@@ -760,9 +789,10 @@ export async function createOpenCodeCliRuntime({ cwd = process.cwd(), io = conso
|
|
|
760
789
|
: interactive.find(item => item.id === 'experiential-labs') || interactive[0];
|
|
761
790
|
if (!target && interactive.length > 1) {
|
|
762
791
|
const prompt = readlineFactory({ input: stdin, output: stdout });
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
const
|
|
792
|
+
const defaultIndex = interactive.indexOf(integration);
|
|
793
|
+
const choices = interactive.map((item, index) => `${index + 1}) ${item.name}${item.id === 'experiential-labs' ? ' (recommended)' : ''}`).join('\n');
|
|
794
|
+
const selected = await questionWithAbort(prompt, `Select provider:\n${choices}\nProvider number [${defaultIndex + 1}]: `, controller.signal);
|
|
795
|
+
const index = selected.trim() === '' ? defaultIndex : Number(selected) - 1;
|
|
766
796
|
if (!Number.isInteger(index) || index < 0 || index >= interactive.length) throw new Error('Select a listed provider number.');
|
|
767
797
|
integration = interactive[index];
|
|
768
798
|
}
|
package/lib/opencode/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const BOOL_FLAGS = new Map([
|
|
|
26
26
|
['--protect-existing', ['protectExisting', true]], ['--jev', ['jev', true]], ['--no-jev', ['jev', false]],
|
|
27
27
|
['--print', ['print', true]], ['--demo', ['demo', true]], ['-c', ['continue', true]],
|
|
28
28
|
['--continue', ['continue', true]], ['-r', ['resume', true]], ['--resume', ['resume', true]],
|
|
29
|
+
['--reset-budget', ['resetBudget', true]],
|
|
29
30
|
]);
|
|
30
31
|
|
|
31
32
|
const aliases = new Map([
|
|
@@ -84,9 +85,9 @@ export function cliHelp(version = '') {
|
|
|
84
85
|
\x1b[32mhash\x1b[0m, \x1b[32mfingerprint\x1b[0m Calculate workspace SHA-256 fingerprint
|
|
85
86
|
\x1b[32mvalidate\x1b[0m <file.json> Validate acceptance contract
|
|
86
87
|
\x1b[32mlogin\x1b[0m, \x1b[32mauth\x1b[0m Connect an AI provider
|
|
87
|
-
\x1b[32maccount\x1b[0m Show
|
|
88
|
-
\x1b[32mbilling\x1b[0m [checkout|portal] Open
|
|
89
|
-
\x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named
|
|
88
|
+
\x1b[32maccount\x1b[0m Show Cloud credit, plan, grants and CLI sessions
|
|
89
|
+
\x1b[32mbilling\x1b[0m [checkout|portal|refresh] Open or refresh Cloud billing
|
|
90
|
+
\x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named Cloud session
|
|
90
91
|
\x1b[32marchive\x1b[0m [list|search|read|export] Read-only legacy session archive
|
|
91
92
|
\x1b[32mreview\x1b[0m Configure self-review or review a PR
|
|
92
93
|
\x1b[32mserve\x1b[0m, \x1b[32mweb\x1b[0m, \x1b[32mstart\x1b[0m Start the web dashboard
|
|
@@ -110,6 +111,8 @@ export function cliHelp(version = '') {
|
|
|
110
111
|
--max-tools <n> Session tool-call budget (0 disables)
|
|
111
112
|
--max-seconds <n> Session elapsed-time budget (0 disables)
|
|
112
113
|
--max-repairs <n> Automatic-repair budget (0 disables)
|
|
114
|
+
--reset-budget Explicitly reset a resumed session's execution budget
|
|
115
|
+
Budgets persist across continuation, including idle time.
|
|
113
116
|
--protect-existing Preserve files present at session start
|
|
114
117
|
--rewrite-cap <n> Whole-file writes per path (0 disables)
|
|
115
118
|
--tool-output-cap <n> Bound text output per tool result
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"releaseVersion": "1.1.0-beta.
|
|
3
|
+
"releaseVersion": "1.1.0-beta.4",
|
|
4
4
|
"compatibilityVersion": "2.0.32",
|
|
5
5
|
"runtimeVersion": "0.0.0-ctg.2.0.32",
|
|
6
6
|
"sourceRevision": "843a6c514e606b436a7fe713422ace0a93ad3d61",
|
|
7
|
-
"patchSha256": "
|
|
7
|
+
"patchSha256": "31e15c7f92f82177bf1b6106732926951c4d751bde36b61f3e1e283818ea5631",
|
|
8
8
|
"lockSha256": "9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d",
|
|
9
9
|
"assets": {
|
|
10
10
|
"linux-x64": {
|
|
11
11
|
"name": "opencode-linux-x64.tar.gz",
|
|
12
|
-
"archiveSha256": "
|
|
13
|
-
"executableSha256": "
|
|
12
|
+
"archiveSha256": "abdecf765542afe7e843059e73ecdd35d189ae8d02a7d41524cd7c28f02fce99",
|
|
13
|
+
"executableSha256": "9f998cb2d23e2c18258a5bcb9a9588228e6fef9f089b13bb2f74b80d8ee8d7f6"
|
|
14
14
|
},
|
|
15
15
|
"linux-arm64": {
|
|
16
16
|
"name": "opencode-linux-arm64.tar.gz",
|
|
17
|
-
"archiveSha256": "
|
|
18
|
-
"executableSha256": "
|
|
17
|
+
"archiveSha256": "d5e2315f3df07cb79c50c6b5221e1f5bb5ce029997ff30eaba5c71834d95d467",
|
|
18
|
+
"executableSha256": "38d4707b301966145cd6cc1dbf23c656a52f42d536c5ae219507f0473ebae97d"
|
|
19
19
|
},
|
|
20
20
|
"darwin-x64": {
|
|
21
21
|
"name": "opencode-darwin-x64.tar.gz",
|
|
22
|
-
"archiveSha256": "
|
|
23
|
-
"executableSha256": "
|
|
22
|
+
"archiveSha256": "568b1e3a5103421f0a08627b985b53becd727097644c1e5e6f5d5be393ecaea3",
|
|
23
|
+
"executableSha256": "4f69cbae17e1c83149cf1066adc117b0e35173158315fe8e841606510f34f770"
|
|
24
24
|
},
|
|
25
25
|
"darwin-arm64": {
|
|
26
26
|
"name": "opencode-darwin-arm64.tar.gz",
|
|
27
|
-
"archiveSha256": "
|
|
28
|
-
"executableSha256": "
|
|
27
|
+
"archiveSha256": "1b164b4bcec032d7bc8276a6e585d7a469625a22408c3499f210bea562c96075",
|
|
28
|
+
"executableSha256": "42733a9a95257373f62ed9663ffbed2478456c537ee7f0d3f3bbdf91418de697"
|
|
29
29
|
},
|
|
30
30
|
"windows-x64": {
|
|
31
31
|
"name": "opencode-windows-x64.tar.gz",
|
|
32
|
-
"archiveSha256": "
|
|
33
|
-
"executableSha256": "
|
|
32
|
+
"archiveSha256": "535ecb051cf4f43f2acf3c18bec599a6739d7ff6565e32852e041addcc868806",
|
|
33
|
+
"executableSha256": "64b3052060df8ed07406c4b42b3a1d56e3500a7e2cd732756d615d443d9c0f33"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
package/lib/opencode/runtime.mjs
CHANGED
|
@@ -13,12 +13,12 @@ export const OPENCODE_VERSION = '2.0.32';
|
|
|
13
13
|
export const CTG_RUNTIME_VERSION = '0.0.0-ctg.2.0.32';
|
|
14
14
|
export const CTG_RUNTIME_SOURCE = '843a6c514e606b436a7fe713422ace0a93ad3d61';
|
|
15
15
|
export const CTG_RUNTIME_LOCK_SHA256 = '9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d';
|
|
16
|
-
export const CTG_RUNTIME_PATCH_SHA256 = '
|
|
16
|
+
export const CTG_RUNTIME_PATCH_SHA256 = '31e15c7f92f82177bf1b6106732926951c4d751bde36b61f3e1e283818ea5631';
|
|
17
17
|
// Update only this development pin after reviewing a new checked-in patch and
|
|
18
18
|
// script/ctg/prepare-development-runtime.sh. The published release pin above,
|
|
19
19
|
// its build recipe, and the release manifest stay immutable until a separately
|
|
20
20
|
// verified runtime release is promoted.
|
|
21
|
-
export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = '
|
|
21
|
+
export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = '31e15c7f92f82177bf1b6106732926951c4d751bde36b61f3e1e283818ea5631';
|
|
22
22
|
const DEFAULT_ROOT = path.join(homedir(), '.carthagent', 'opencode');
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { Plugin } from "@opencode/plugin/tui";
|
|
|
3
3
|
import { Show } from "solid-js";
|
|
4
4
|
import { writeFile } from "node:fs/promises";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import { nativeProviderName } from "../../../lib/opencode/provider-policy.mjs";
|
|
6
|
+
import { nativeLoginProvider, nativeProviderName } from "../../../lib/opencode/provider-policy.mjs";
|
|
7
7
|
import { framePrompt } from "../../../lib/tui/framing.mjs";
|
|
8
8
|
import { carthagentConfigPath, loadCarthagentConfig, resolveReviewMode, reviewKickoff, saveCarthagentConfig } from "../../../lib/review.mjs";
|
|
9
9
|
import { COMMANDS, NATIVE_COMMAND_COLLISIONS } from "./ui/commands.js";
|
|
@@ -114,7 +114,7 @@ export default Plugin.define({
|
|
|
114
114
|
context.ui.router.navigate({ type: "plugin", name: "daily-quests", data: { ...(sessionID ? { sessionID } : {}) } });
|
|
115
115
|
return;
|
|
116
116
|
case "login":
|
|
117
|
-
context.keymap.dispatch("provider.connect");
|
|
117
|
+
context.keymap.dispatch("provider.connect", arg ? nativeLoginProvider(arg)?.id ?? arg : "experiential-labs");
|
|
118
118
|
return;
|
|
119
119
|
case "help": case "ctg-help":
|
|
120
120
|
await context.ui.dialog.alert({ title: "Carthagent commands", message: COMMANDS.map((command) => `/${command.name}${command.args ? ` ${command.args}` : ""}${command.aliases?.length ? ` (${command.aliases.map((alias) => `/${alias}`).join(", ")})` : ""} — ${command.description}`).join("\n") + "\n\nCtrl+R sessions · Ctrl+T settings · Ctrl+N new session are native Carthagent actions." });
|
|
@@ -6,7 +6,7 @@ set -euo pipefail
|
|
|
6
6
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
7
7
|
EXPECTED_SOURCE="843a6c514e606b436a7fe713422ace0a93ad3d61"
|
|
8
8
|
EXPECTED_LOCK="9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d"
|
|
9
|
-
EXPECTED_PATCH="
|
|
9
|
+
EXPECTED_PATCH="31e15c7f92f82177bf1b6106732926951c4d751bde36b61f3e1e283818ea5631"
|
|
10
10
|
TARGET="${1:-opencode-linux-x64}"
|
|
11
11
|
HOST_PLATFORM="$(node -p 'process.platform')"
|
|
12
12
|
HOST_ARCH="$(node -p 'process.arch')"
|
|
@@ -34,7 +34,7 @@ if ! git diff --cached --quiet HEAD --; then
|
|
|
34
34
|
exit 1
|
|
35
35
|
fi
|
|
36
36
|
expected_inputs=(
|
|
37
|
-
"docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
37
|
+
"docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch"
|
|
38
38
|
"script/ctg/build-owned-runtime.sh"
|
|
39
39
|
)
|
|
40
40
|
actual_status="$(git status --porcelain --untracked-files=all)"
|
|
@@ -86,7 +86,7 @@ if [[ "$(bun --version)" != "1.4.2" ]]; then
|
|
|
86
86
|
exit 1
|
|
87
87
|
fi
|
|
88
88
|
|
|
89
|
-
patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
89
|
+
patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch"
|
|
90
90
|
actual_patch="$("${HASH_COMMAND[@]}" "$patch_file" | awk '{print $1}')"
|
|
91
91
|
if [[ "$actual_patch" != "$EXPECTED_PATCH" ]]; then
|
|
92
92
|
echo "Patch checksum mismatch: $actual_patch" >&2
|
package/tests/cli.test.mjs
CHANGED
|
@@ -102,11 +102,11 @@ test('check fails clearly when no delivery report exists', t => {
|
|
|
102
102
|
assert.match(result.stderr, /No delivery report/);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test('guidance routing is not a user-facing command and
|
|
105
|
+
test('guidance routing is not a user-facing command and Cloud account commands are documented', t => {
|
|
106
106
|
const result = run(fixture(t), '--help');
|
|
107
107
|
assert.equal(result.status, 0, result.stderr);
|
|
108
108
|
assert.doesNotMatch(result.stdout, /\broute\b|\bprofiles\b/);
|
|
109
|
-
assert.match(result.stdout, /account.*
|
|
109
|
+
assert.match(result.stdout, /account.*Cloud credit/);
|
|
110
110
|
assert.match(result.stdout, /billing.*checkout\|portal/);
|
|
111
111
|
assert.match(result.stdout, /logout-cloud/);
|
|
112
112
|
});
|
|
@@ -10,7 +10,7 @@ import ts from 'typescript';
|
|
|
10
10
|
import { parseOpenCodeArgs } from '../lib/opencode/cli.mjs';
|
|
11
11
|
import { normalizeSessionOptions } from '../lib/opencode/session-options.mjs';
|
|
12
12
|
|
|
13
|
-
const patch = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
13
|
+
const patch = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch', import.meta.url), 'utf8');
|
|
14
14
|
const path = 'packages/tui/src/routes/session/plan-items.ts';
|
|
15
15
|
const section = patch.split(`diff --git a/${path} b/${path}\n`)[1]?.split('\ndiff --git ')[0];
|
|
16
16
|
assert.ok(section, 'the plan graph parser is part of the reproducible runtime patch');
|
|
@@ -72,14 +72,19 @@ test('development runtime source and preparation recipe pin the actual Ship grap
|
|
|
72
72
|
assert.equal(CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256, digest);
|
|
73
73
|
const prepare = readFileSync(new URL('../script/ctg/prepare-development-runtime.sh', import.meta.url), 'utf8');
|
|
74
74
|
assert.ok(prepare.includes(`EXPECTED_PATCH="${digest}"`));
|
|
75
|
-
assert.ok(prepare.includes('docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
75
|
+
assert.ok(prepare.includes('docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch'));
|
|
76
76
|
assert.doesNotMatch(prepare, /bun (install|run|build)|gh release|npm publish/);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test('the
|
|
80
|
-
const published = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
79
|
+
test('the published runtime pin stays on its immutable release patch', () => {
|
|
80
|
+
const published = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch', import.meta.url));
|
|
81
81
|
const digest = createHash('sha256').update(published).digest('hex');
|
|
82
82
|
assert.equal(CTG_RUNTIME_PATCH_SHA256, digest);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the candidate build recipe pins the reviewed onboarding patch', () => {
|
|
86
|
+
const digest = createHash('sha256').update(patch).digest('hex');
|
|
83
87
|
const build = readFileSync(new URL('../script/ctg/build-owned-runtime.sh', import.meta.url), 'utf8');
|
|
84
88
|
assert.ok(build.includes(`EXPECTED_PATCH="${digest}"`));
|
|
89
|
+
assert.ok(build.includes('docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch'));
|
|
85
90
|
});
|
|
@@ -42,6 +42,40 @@ test('native CLI selects headless execution and preserves output-mode options',
|
|
|
42
42
|
assert.equal(invocation.prompt, 'Do work');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test('headless continuation explains persistent budget exhaustion and resets only on explicit request', async t => {
|
|
46
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
47
|
+
const { tmpdir } = await import('node:os');
|
|
48
|
+
const cwd = mkdtempSync(join(tmpdir(), 'ctg-budget-continuation-'));
|
|
49
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
50
|
+
const { io, output } = ioCapture();
|
|
51
|
+
const calls = [];
|
|
52
|
+
let stopped = 'elapsed-time';
|
|
53
|
+
const runtime = await createOpenCodeCliRuntime({ cwd, io, stdout: { write() {} }, native: {
|
|
54
|
+
runtime: {}, service: { ensureService: async () => ({}) }, rpcDefinitions: { Carthagent: {} },
|
|
55
|
+
clientFactory: async () => ({
|
|
56
|
+
rpc: () => ({ budgetStatus: async () => ({ stopped }) }),
|
|
57
|
+
session: {
|
|
58
|
+
list: async () => ({ data: [{ id: 'same-session', location: { directory: cwd } }] }),
|
|
59
|
+
get: async () => ({ id: 'same-session', location: { directory: cwd } }),
|
|
60
|
+
command: async input => { calls.push(input); stopped = null; },
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
headless: { runHeadless: async (_service, _cwd, _prompt, options) => {
|
|
64
|
+
assert.equal(options.sessionID, 'same-session');
|
|
65
|
+
calls.push('prompt');
|
|
66
|
+
return { session: { id: 'same-session' }, outcome: stopped ? 'interrupted' : 'succeeded', terminal: true,
|
|
67
|
+
messages: stopped ? [] : [{ type: 'assistant', finish: 'stop', content: [{ type: 'text', text: 'continued' }] }] };
|
|
68
|
+
} },
|
|
69
|
+
} });
|
|
70
|
+
assert.equal(parseOpenCodeArgs(['--continue', '--reset-budget', 'Continue']).resetBudget, true);
|
|
71
|
+
assert.equal(await runtime.run({ mode: 'headless', options: { continue: true }, prompt: 'Continue' }), 1);
|
|
72
|
+
assert.match(output.stderr.at(-1), /budget exhausted: elapsed-time.*--reset-budget/);
|
|
73
|
+
assert.deepEqual(calls, ['prompt'], 'no implicit budget reset or prompt retry');
|
|
74
|
+
assert.equal(await runtime.run({ mode: 'headless', options: { continue: true, resetBudget: true }, prompt: 'Continue' }), 0);
|
|
75
|
+
assert.deepEqual(calls.slice(1), [{ sessionID: 'same-session', name: 'delivery-budget-reset', text: '' }, 'prompt']);
|
|
76
|
+
await assert.rejects(runtime.run({ mode: 'headless', options: { resetBudget: true }, prompt: 'New task' }), /requires --session/);
|
|
77
|
+
});
|
|
78
|
+
|
|
45
79
|
test('headless CLI validates prompts and maps cancellation to conventional exit code', async () => {
|
|
46
80
|
const { io, output } = ioCapture();
|
|
47
81
|
assert.equal(await runOpenCodeCli({ argv: [], stdoutIsTTY: false, io, runtime: {} }), 1);
|
|
@@ -77,10 +111,10 @@ test('model, auth, cloud and domain entrypoints dispatch to injected native capa
|
|
|
77
111
|
]);
|
|
78
112
|
});
|
|
79
113
|
|
|
80
|
-
test('help preserves command aliases,
|
|
114
|
+
test('help preserves command aliases, Cloud operations and isolation semantics', async () => {
|
|
81
115
|
const { output, io } = ioCapture();
|
|
82
116
|
assert.equal(await runOpenCodeCli({ argv: ['--help'], io, version: '1.0.3' }), 0);
|
|
83
|
-
assert.match(output.stdout[0], /account.*
|
|
117
|
+
assert.match(output.stdout[0], /account.*Cloud credit/);
|
|
84
118
|
assert.match(output.stdout[0], /billing.*checkout\|portal/);
|
|
85
119
|
assert.match(output.stdout[0], /--isolate\s+Isolate extension\/skill discovery \(not a worktree\)/);
|
|
86
120
|
assert.equal(cliHelp('x').includes('--engine-cli, --agent-cli'), true);
|
|
@@ -518,6 +552,27 @@ test('no-prompt interactive explicit model and thinking selection applies to fre
|
|
|
518
552
|
}
|
|
519
553
|
});
|
|
520
554
|
|
|
555
|
+
test('headless continuation switches an explicitly requested model before submitting to the same session', async () => {
|
|
556
|
+
const calls = [];
|
|
557
|
+
const runtime = await createOpenCodeCliRuntime({ cwd: '/project', stdout: { write() {} }, native: {
|
|
558
|
+
runtime: {}, service: { ensureService: async () => ({ url: 'http://127.0.0.1:1', headers: {} }) },
|
|
559
|
+
headless: { runHeadless: async (_service, _cwd, _prompt, options) => {
|
|
560
|
+
calls.push(['run', options.sessionID]);
|
|
561
|
+
assert.equal(options.timeout, 30 * 60_000);
|
|
562
|
+
return { session: { id: options.sessionID }, outcome: 'failed', terminal: true, messages: [] };
|
|
563
|
+
} },
|
|
564
|
+
clientFactory: async () => ({ session: {
|
|
565
|
+
get: async () => ({ id: 'ses_existing', location: { directory: '/project' }, metadata: {} }),
|
|
566
|
+
switchModel: async input => calls.push(['switch', input]),
|
|
567
|
+
} }),
|
|
568
|
+
} });
|
|
569
|
+
await runtime.run({ mode: 'headless', options: { session: 'ses_existing', provider: 'fixture', model: 'cheaper-model' }, prompt: 'continue' });
|
|
570
|
+
assert.deepEqual(calls, [
|
|
571
|
+
['switch', { sessionID: 'ses_existing', model: { providerID: 'fixture', id: 'cheaper-model' } }],
|
|
572
|
+
['run', 'ses_existing'],
|
|
573
|
+
]);
|
|
574
|
+
});
|
|
575
|
+
|
|
521
576
|
test('resumed interactive model selection verifies workspace ownership before any session mutation or launch', async t => {
|
|
522
577
|
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
523
578
|
const { tmpdir } = await import('node:os');
|
|
@@ -542,6 +597,11 @@ test('resumed interactive model selection verifies workspace ownership before an
|
|
|
542
597
|
mode: 'interactive', options: { session: 'ses_foreign', model: 'fixture/explicit-model', maxTools: '3' }, prompt: '',
|
|
543
598
|
}), /another workspace/);
|
|
544
599
|
assert.deepEqual(calls, [['get', 'ses_foreign']], 'ownership is checked before switching models, updating metadata, or launching TUI');
|
|
600
|
+
calls.length = 0;
|
|
601
|
+
await assert.rejects(runtime.run({
|
|
602
|
+
mode: 'headless', options: { session: 'ses_foreign', model: 'fixture/explicit-model' }, prompt: 'continue',
|
|
603
|
+
}), /another workspace/);
|
|
604
|
+
assert.deepEqual(calls, [['get', 'ses_foreign']], 'headless model switches enforce the same workspace boundary');
|
|
545
605
|
});
|
|
546
606
|
|
|
547
607
|
test('interactive provider or thinking flags without a model fail instead of being silently ignored', async () => {
|
|
@@ -752,18 +812,19 @@ test('resumed native sessions merge explicit launcher options into their own wor
|
|
|
752
812
|
assert.deepEqual(framingOverrides, [{ sessionID: 'ses_existing', enabled: false }]);
|
|
753
813
|
});
|
|
754
814
|
|
|
755
|
-
test('native login
|
|
815
|
+
test('native login defaults to Cloud on Enter and completes the host OAuth lifecycle', async () => {
|
|
756
816
|
const calls = [];
|
|
757
817
|
const opened = [];
|
|
818
|
+
const prompts = [];
|
|
758
819
|
const service = { url: 'http://127.0.0.1:1', headers: {} };
|
|
759
820
|
const integration = {
|
|
760
821
|
id: 'experiential-labs', name: 'Carthagent Ship',
|
|
761
822
|
methods: [{ id: 'carthagent-device', type: 'oauth', label: 'Sign in with Carthagent Ship' }],
|
|
762
823
|
};
|
|
763
|
-
const runtime = await createOpenCodeCliRuntime({ cwd: '/project', io: ioCapture().io, stdin: { isTTY: true }, stdout: { isTTY: true }, openBrowserImpl: async url => { opened.push(url); return true; }, native: {
|
|
824
|
+
const runtime = await createOpenCodeCliRuntime({ cwd: '/project', io: ioCapture().io, stdin: { isTTY: true }, stdout: { isTTY: true }, readlineFactory: () => ({ close() {}, question: async text => { prompts.push(text); return ''; } }), openBrowserImpl: async url => { opened.push(url); return true; }, native: {
|
|
764
825
|
runtime: {}, service: { ensureService: async () => service }, headless: {},
|
|
765
826
|
clientFactory: async () => ({ integration: {
|
|
766
|
-
list: async input => { calls.push(['list', input]); return { data: [integration] }; },
|
|
827
|
+
list: async input => { calls.push(['list', input]); return { data: [{ id: 'openai', name: 'OpenAI', methods: [{ type: 'key' }] }, integration] }; },
|
|
767
828
|
oauth: {
|
|
768
829
|
connect: async input => { calls.push(['connect', input]); return { data: {
|
|
769
830
|
attemptID: 'attempt-1', url: 'https://auth.example/device', instructions: 'Approve access', mode: 'auto',
|
|
@@ -775,6 +836,9 @@ test('native login uses OpenCode integration OAuth status and completion lifecyc
|
|
|
775
836
|
} }),
|
|
776
837
|
} });
|
|
777
838
|
assert.equal(await runtime.login(), 0);
|
|
839
|
+
assert.match(prompts[0], /1\) Carthagent Cloud \(recommended\)/);
|
|
840
|
+
assert.match(prompts[0], /Provider number \[1\]:/);
|
|
841
|
+
assert.equal(calls[1][1].integrationID, 'experiential-labs');
|
|
778
842
|
assert.deepEqual(opened, ['https://auth.example/device']);
|
|
779
843
|
assert.deepEqual(calls.map(([name]) => name), ['list', 'connect', 'status']);
|
|
780
844
|
assert.equal(calls[1][1].methodID, 'carthagent-device');
|
|
@@ -106,7 +106,7 @@ function workflowJob(name) {
|
|
|
106
106
|
return lines.slice(start, end).join('\n');
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const combinedPatchPath = 'docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
109
|
+
const combinedPatchPath = 'docs/patches/opencode-v2.0.32-session-ui-combined-v6.patch';
|
|
110
110
|
|
|
111
111
|
function sha256(bytes) {
|
|
112
112
|
return createHash('sha256').update(bytes).digest('hex');
|
|
@@ -181,7 +181,13 @@ async function fixture(t) {
|
|
|
181
181
|
execFileSync('git', ['init', '-q'], { cwd: ctg });
|
|
182
182
|
execFileSync('git', ['-c', 'user.name=Workflow Fixture', '-c', 'user.email=fixture@example.invalid', 'add', combinedPatchPath, 'script/ctg/build-owned-runtime.sh'], { cwd: ctg });
|
|
183
183
|
execFileSync('git', ['-c', 'user.name=Workflow Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'fixture candidate patch'], { cwd: ctg });
|
|
184
|
-
|
|
184
|
+
// Publication uses a final package whose trust pin has been promoted after
|
|
185
|
+
// prebuild; the working tree may still retain the previous public release.
|
|
186
|
+
const runtimeSource = await readFile(path.join(root, 'lib/opencode/runtime.mjs'), 'utf8');
|
|
187
|
+
await writeFile(path.join(ctg, 'lib/opencode/runtime.mjs'), runtimeSource.replace(
|
|
188
|
+
/CTG_RUNTIME_PATCH_SHA256 = '[a-f0-9]{64}'/,
|
|
189
|
+
`CTG_RUNTIME_PATCH_SHA256 = '${patchSha256}'`,
|
|
190
|
+
));
|
|
185
191
|
|
|
186
192
|
const manifest = {
|
|
187
193
|
version: 1,
|
|
@@ -403,9 +409,9 @@ test('draft metadata validation has scoped write capability; artifact mutation s
|
|
|
403
409
|
assert.doesNotMatch(assembly, /contents: write/);
|
|
404
410
|
assert.match(publish, /contents: write/);
|
|
405
411
|
assert.match(uploadBlock, /gh release upload/);
|
|
406
|
-
assert.match(workflow, /git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
407
|
-
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
408
|
-
assert.equal((workflow.match(/git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
412
|
+
assert.match(workflow, /git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v6\.patch > opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v6\.patch/);
|
|
413
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v6\.patch\)"/);
|
|
414
|
+
assert.equal((workflow.match(/git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v6\.patch > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/g) ?? []).length, 1,
|
|
409
415
|
'candidate assembly must extract exact committed bytes at the pinned historical path');
|
|
410
416
|
assert.match(verifyBlock, /git -C ctg show HEAD:script\/ctg\/build-owned-runtime\.sh/);
|
|
411
417
|
assert.match(verifyBlock, /git -C ctg show "HEAD:\$patch_file" > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/);
|
|
@@ -413,7 +419,7 @@ test('draft metadata validation has scoped write capability; artifact mutation s
|
|
|
413
419
|
assert.match(workflow, /chmod \+x opencode\/script\/ctg\/build-owned-runtime\.sh/);
|
|
414
420
|
assert.match(workflow, /GIT_CONFIG_KEY_0:\s*core\.autocrlf[\s\S]*?GIT_CONFIG_VALUE_0:\s*'false'/);
|
|
415
421
|
assert.match(workflow, /git -C opencode config --local core\.autocrlf false/);
|
|
416
|
-
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
422
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v6\.patch\)"/);
|
|
417
423
|
});
|
|
418
424
|
|
|
419
425
|
test('review-branch prebuild source is exact dispatch SHA; publish validation is default-branch-only', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
@@ -83,7 +83,7 @@ async function fixture(t, patchSha256 = developmentPatch) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
test('packaged release trust stays valid independently of the development patch pin', async t => {
|
|
86
|
-
assert.equal(CTG_RUNTIME_PATCH_SHA256, '
|
|
86
|
+
assert.equal(CTG_RUNTIME_PATCH_SHA256, '31e15c7f92f82177bf1b6106732926951c4d751bde36b61f3e1e283818ea5631');
|
|
87
87
|
const published = await readRuntimeReleaseManifest();
|
|
88
88
|
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
89
89
|
assert.equal(published.releaseVersion, packageJson.version);
|