carthagent 1.1.0-preview.12 → 1.1.0-preview.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -13
- package/docs/native-manual-testing.md +27 -5
- package/docs/opencode-owned-runtime.md +3 -3
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/source-preview.md +120 -0
- package/lib/delivery-review.mjs +11 -1
- package/lib/opencode/runtime-release.json +12 -12
- package/lib/opencode/runtime.mjs +5 -4
- package/lib/providers/experiential.mjs +2 -2
- package/package.json +1 -1
- package/plugins/carthagent/src/daily-source-rpc.ts +2 -2
- package/plugins/carthagent/src/server/daily-source-rpc.ts +18 -4
- package/plugins/carthagent/src/server/delivery.ts +10 -6
- package/plugins/carthagent/src/server/providers.ts +1 -1
- package/plugins/carthagent/src/server/ship-guidance.ts +4 -2
- package/plugins/carthagent/src/tui.tsx +17 -13
- package/plugins/carthagent/src/ui/daily-quests.tsx +5 -2
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +2 -0
- package/plugins/carthagent/src/ui/native-review.mjs +7 -2
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +11 -2
- package/plugins/carthagent/src/ui/voyages.tsx +4 -2
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +20 -9
- package/script/ctg/build-owned-runtime.sh +3 -3
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/native-daily-source.test.mjs +27 -2
- package/tests/native-manual-projects.test.mjs +3 -3
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-review-consumer.test.mjs +19 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-plan.test.mjs +13 -3
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/owned-runtime-publish-workflow.test.mjs +6 -6
- package/tests/runtime-development-trust.test.mjs +5 -5
- 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/update.test.mjs +6 -5
- package/tools/benchmark/planning-startup.test.mjs +1 -1
- package/tools/test-native-delivery-host.mjs +2 -2
- package/tools/test-native-host.mjs +4 -2
- package/tools/test-native-public-repo.mjs +1 -1
- package/tools/test-native-ship-walkthrough.mjs +1 -1
- package/tools/test-native-swe-host.mjs +4 -4
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 1.1.0-preview.13 experimental release
|
|
2
|
+
|
|
3
|
+
Experimental preview. Install it explicitly with the experimental channel;
|
|
4
|
+
stable 1.0.7 is unchanged and published preview.12 remains immutable.
|
|
5
|
+
|
|
6
|
+
## What changes
|
|
7
|
+
|
|
8
|
+
**Directory-first Projects rail.** The native left rail is organized around
|
|
9
|
+
folders: a current-folder card with Browse, New session and model controls,
|
|
10
|
+
distinct folder and group headings, and an active-session marker. A
|
|
11
|
+
folder-browser dialog adds Back, Forward, Up, Home, direct paths and
|
|
12
|
+
filtering, with async cancellation, exact path handling and preserved drafts.
|
|
13
|
+
|
|
14
|
+
**Shared folder/model setup for new sessions.** Global and per-folder New
|
|
15
|
+
session entries route through one setup flow. The session is created only on
|
|
16
|
+
the first send, keeping the chosen folder, agent, model and applicable
|
|
17
|
+
variant; unavailable selections are blocked instead of silently substituted.
|
|
18
|
+
Pre-session `/model` is supported.
|
|
19
|
+
|
|
20
|
+
**Isolated source-preview launcher.** `script/ctg/source-preview.sh` runs the
|
|
21
|
+
checked-in development patch against its own profile for verification without
|
|
22
|
+
touching the published runtime or its caches. The combined-v5 patch is a
|
|
23
|
+
replacement patch: it contains all of combined-v4 plus these sources and
|
|
24
|
+
their tests, and applies directly to the pinned upstream.
|
|
25
|
+
|
|
26
|
+
The upstream revision, lockfile and detached runtime identity are unchanged
|
|
27
|
+
from preview.12; the runtime is rebuilt because each runtime manifest is
|
|
28
|
+
bound to its exact package version.
|
|
29
|
+
|
|
30
|
+
**Ship and Voyages follow-up fixes.** Beam replay requests include the required
|
|
31
|
+
nonempty reasoning placeholder when earlier assistant messages have no recorded
|
|
32
|
+
reasoning. The delivery plan entry point stays visible in the code-mode catalog,
|
|
33
|
+
and final review accepts single-assertion aliases and assertion arrays. Voyages
|
|
34
|
+
and quest lists activate on mouse release so newly opened dialogs remain open;
|
|
35
|
+
rail clicks select the intended quest and respect modal focus.
|
|
36
|
+
Delivery tool guidance explicitly identifies JSON-text responses and requires
|
|
37
|
+
relative file paths for promised outputs, avoiding prose-as-filename contracts.
|
|
38
|
+
|
|
39
|
+
## Provenance
|
|
40
|
+
|
|
41
|
+
| Input | Exact value |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Package | `1.1.0-preview.13` |
|
|
44
|
+
| Prebuild source | `e5de273811a11c2f8678b54605c0068ab8cfcf38` |
|
|
45
|
+
| Prebuild workflow run | [41380550073](https://github.com/B-icy/Carthagent/actions/runs/41380550073) |
|
|
46
|
+
| Full manifest SHA-256 | `5083dbe10b6f4be2550ab0abb04c9299197dacabe4b9d3556bd3b03fc9f6151a` |
|
|
47
|
+
| Upstream revision | `843a6c514e606b436a7fe713422ace0a93ad3d61` |
|
|
48
|
+
| CTG patch (combined-v5) SHA-256 | `ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975` |
|
|
49
|
+
| Lock SHA-256 | `9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d` |
|
|
50
|
+
| Runtime / compatibility | `0.0.0-ctg.2.0.32` / `2.0.32` |
|
|
51
|
+
|
|
52
|
+
## Trying it
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
ctg update --channel experimental --check
|
|
56
|
+
ctg update --channel experimental
|
|
57
|
+
ctg --version # 1.1.0-preview.13
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or install the exact version from npm or the exact tag:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install -g carthagent@1.1.0-preview.13
|
|
64
|
+
npm install -g --install-links=true github:B-icy/Carthagent#v1.1.0-preview.13
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`ctg update --channel stable` returns to stable 1.0.7 without `--force`. See
|
|
68
|
+
the [upgrade guide](native-upgrade.md) for profile preservation.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Projects sidebar source preview
|
|
2
|
+
|
|
3
|
+
This preview runs the redesigned Projects sidebar from OpenCode source plus the
|
|
4
|
+
Carthagent plugin in this checkout. It is a development preview. Nothing here
|
|
5
|
+
builds, publishes, installs, or changes the published `ctg` runtime.
|
|
6
|
+
|
|
7
|
+
## What is pinned where
|
|
8
|
+
|
|
9
|
+
| Purpose | Patch | Recipe | Hash reference |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| Published runtime (preview.13) | `docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch` | `script/ctg/build-owned-runtime.sh`, publish workflow | `CTG_RUNTIME_PATCH_SHA256`, `lib/opencode/runtime-release.json` |
|
|
12
|
+
| Source preview (development) | `docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch` | `script/ctg/prepare-development-runtime.sh` | `CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256` |
|
|
13
|
+
|
|
14
|
+
Both patches apply to clean upstream `https://github.com/anomalyco/opencode` at
|
|
15
|
+
`843a6c514e606b436a7fe713422ace0a93ad3d61`. v5 is a replacement patch: it
|
|
16
|
+
contains everything in v4 plus the sidebar, folder browser and new-session
|
|
17
|
+
setup sources and their tests under `packages/tui/test/sidebar`. Do not apply
|
|
18
|
+
v4 first.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
1. `npm ci` in this checkout (the plugin runs from `plugins/carthagent`).
|
|
23
|
+
2. Clone upstream into a new directory, check out the pinned commit, and run
|
|
24
|
+
`script/ctg/prepare-development-runtime.sh <checkout>`. The script refuses a
|
|
25
|
+
different commit, a dirty checkout or a patch with the wrong SHA-256, then
|
|
26
|
+
runs `git apply --check` and `git apply`. It does not install or compile.
|
|
27
|
+
3. Run `bun install` with Bun 1.4.2 in the prepared checkout.
|
|
28
|
+
|
|
29
|
+
## Launch
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
script/ctg/source-preview.sh --source DIR --profile DIR --bun PATH [--workspace DIR] [-- ARGS...]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- All three of `--source`, `--profile` and `--bun` are required. Relative
|
|
36
|
+
paths resolve against your current directory, so you can launch from
|
|
37
|
+
anywhere. Paths with spaces work when quoted.
|
|
38
|
+
- The launcher checks that `packages/cli/src/index.ts` and `node_modules`
|
|
39
|
+
exist and that `bun --version` matches the checkout's `packageManager` pin.
|
|
40
|
+
It fails before touching the profile when a prerequisite is missing.
|
|
41
|
+
- It runs Bun with `--no-env-file` from `<source>/packages/cli` (required for
|
|
42
|
+
JSX resolution) under `env -i`. Only these variables reach OpenCode: `HOME`,
|
|
43
|
+
`PATH` (Bun's directory, Node's directory if found, `/usr/bin`, `/bin`),
|
|
44
|
+
`TERM`, `COLORTERM`, `TMPDIR`, the four `XDG_*` directories,
|
|
45
|
+
`OPENCODE_CONFIG_DIR`, `OPENCODE_TEST_HOME`,
|
|
46
|
+
`OPENCODE_DISABLE_PROJECT_CONFIG=1`, `OPENCODE_DISABLE_AUTOUPDATE=1` and
|
|
47
|
+
`BUN_RUNTIME_TRANSPILER_CACHE_PATH=0`. Provider API-key variables are never
|
|
48
|
+
passed through.
|
|
49
|
+
- First launch creates the profile (0700) with `config/opencode/opencode.json`
|
|
50
|
+
(the plugin path), `cli.json` (Carthage theme) and `themes/carthage.json`
|
|
51
|
+
(all 0600). Each file is created only if absent; existing configuration,
|
|
52
|
+
sessions and authentication are left untouched. If an existing
|
|
53
|
+
`opencode.json` does not list the plugin, the launcher prints a warning and
|
|
54
|
+
still leaves the file alone.
|
|
55
|
+
- `--standalone` is always used, so the CLI starts its own private server on
|
|
56
|
+
a loopback ephemeral port (`--port 0`) that exits with the TUI. `--server`
|
|
57
|
+
is rejected. Without `--`, the workspace (default: current directory) is
|
|
58
|
+
opened. With `--`, the remaining arguments run another CLI command in the
|
|
59
|
+
same profile, for example `-- auth login`.
|
|
60
|
+
|
|
61
|
+
## Use
|
|
62
|
+
|
|
63
|
+
- **Folder card** (top of Projects): current folder, abbreviated path (click
|
|
64
|
+
for the full path), Browse, New session and the model action. Narrow layouts
|
|
65
|
+
show compact `⌕` search, `▤` Browse and `◇` model controls.
|
|
66
|
+
- **Browse** (`/browse`, palette “Browse folders”): Filter and Path inputs
|
|
67
|
+
(Tab switches), Up/Down select, Enter opens, Alt+Left/Right/Up for
|
|
68
|
+
Back/Forward/Up, Alt+H for Home, Ctrl+S confirms, Escape cancels.
|
|
69
|
+
- **New session** (`Ctrl+X N`, `/new`, palette “New session” or “New session
|
|
70
|
+
in folder…”, folder card, compact `+`, folder right-click menu): choose the
|
|
71
|
+
folder, then the model. Global entries start at the folder a plain new
|
|
72
|
+
session would use; project entries start at that project's folder. Nothing
|
|
73
|
+
is created until the first message is sent; cancelling leaves the current
|
|
74
|
+
session, draft and model unchanged. The new session keeps the agent you were
|
|
75
|
+
using and, when you keep the same model, its variant (including the default).
|
|
76
|
+
If the chosen agent or model is not available at the folder when you send,
|
|
77
|
+
the send is blocked with your text kept and no session created; pick another
|
|
78
|
+
agent (`Shift+Tab`) or model (`/model`, `Ctrl+X M`) and send again. There is
|
|
79
|
+
no automatic fallback to another model. `/model` also works before a session
|
|
80
|
+
exists. The model step inside setup has no integration shortcut; connect
|
|
81
|
+
integrations from the standalone model picker (`Ctrl+X M`).
|
|
82
|
+
- **Exit**: `/exit` or palette “Exit the app”. The private server exits with
|
|
83
|
+
the TUI.
|
|
84
|
+
- **Resume**: relaunch with the same `--profile` and select the session in
|
|
85
|
+
Projects. Automatic resume is not part of this preview.
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
Run serially. Close the preview first.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
# Product (from this checkout)
|
|
93
|
+
node --test --test-concurrency=1 tests/source-preview.test.mjs tests/native-ship-plan.test.mjs tests/native-model-command.test.mjs
|
|
94
|
+
npx eslint . && npm run typecheck
|
|
95
|
+
|
|
96
|
+
# Native (from <source>/packages/tui, using the same isolated variables as the launcher)
|
|
97
|
+
bun --no-env-file test --timeout 30000 test/navigation-rail.test.tsx \
|
|
98
|
+
test/component/session-tabs-rail.test.tsx test/context/session-tabs-model.test.ts \
|
|
99
|
+
test/ui/layout.test.ts test/sidebar
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`tests/source-preview.test.mjs` uses a Bun test double and synthetic profiles,
|
|
103
|
+
so it proves argument handling, isolation and config preservation, not a real
|
|
104
|
+
launch. It runs the launcher with `/bin/sh` on Linux and macOS and is skipped
|
|
105
|
+
on native Windows, where the POSIX launcher is not supported.
|
|
106
|
+
|
|
107
|
+
## Limits
|
|
108
|
+
|
|
109
|
+
- Source preview only. No native binary, package or release includes these
|
|
110
|
+
changes, and the published runtime remains v4.
|
|
111
|
+
- The launcher is a POSIX `sh` script for Linux and macOS (GNU and BSD
|
|
112
|
+
utilities). Native Windows is not supported.
|
|
113
|
+
- Real launches were verified on Linux with Bun 1.4.2. No cross-platform
|
|
114
|
+
launch matrix was run.
|
|
115
|
+
- The native type checker (`tsgo`) and native build are not part of this
|
|
116
|
+
workflow; Bun tests compile the changed modules but are not a full static
|
|
117
|
+
type check.
|
|
118
|
+
- Some native tests outside the focused list fail in the same way on the
|
|
119
|
+
pre-change sources (for example missing `ToastProvider` in
|
|
120
|
+
`session-tabs-status` harnesses).
|
package/lib/delivery-review.mjs
CHANGED
|
@@ -41,13 +41,23 @@ export async function captureDeliveryReview(state, cwd, directory, signal) {
|
|
|
41
41
|
return { ...capture, diff: text.slice(0, 12000), truncated: text.length > 12000, unresolvedFindings: (state.reviewFindings || []).filter(f => !f.resolution), next: 'Read the full capture path if truncated; new text source is included. Inspect binary assets separately. Probe critical component connections and user outcomes, not only individual functions or artifact existence. Record coverage, findings and limitations; earlier findings require explicit resolutions with evidence, never silently move them to limitations.' };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** Models commonly send `assertion` or a string array; both carry the same claim. */
|
|
45
|
+
function coverageAssertions(item) {
|
|
46
|
+
const value = item.assertions ?? item.assertion;
|
|
47
|
+
if (Array.isArray(value)) return value.filter(v => typeof v === 'string' && v.trim()).join('; ');
|
|
48
|
+
return typeof value === 'string' ? value : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
export function recordDeliveryReview(state, cwd, params) {
|
|
45
52
|
const capture = state.reviewCapture;
|
|
46
53
|
const hash = fingerprint(cwd, ['.']);
|
|
47
54
|
if (!capture || capture.id !== params.captureId || capture.runId !== state.runId || capture.revision !== state.revision || capture.fingerprint !== hash) throw Error('Review capture missing or stale. Use delivery_review action=inspect after final source edits.');
|
|
48
55
|
if (pendingRequirements(state, hash).length) throw Error('Run fresh checks for every requirement before recording review.');
|
|
49
56
|
const nonempty = value => typeof value === 'string' && value.trim().length > 0;
|
|
50
|
-
if (!Array.isArray(params.coverage) ||
|
|
57
|
+
if (!Array.isArray(params.coverage) || params.coverage.some(c => !c || typeof c !== 'object')) throw Error('coverage must be an array of {requirement, assertions, status?} objects.');
|
|
58
|
+
params = { ...params, coverage: params.coverage.map(c => ({ ...c, assertions: coverageAssertions(c) })) };
|
|
59
|
+
const uncovered = (state.plan.acceptance || []).filter(a => !params.coverage.some(c => c.requirement === a.requirement && nonempty(c.assertions))).map(a => a.requirement);
|
|
60
|
+
if (uncovered.length) throw Error(`Review coverage must explain concrete assertions for every exact acceptance requirement, including required validators. Missing: ${JSON.stringify(uncovered.slice(0, 10))}${uncovered.length > 10 ? ` (+${uncovered.length - 10} more)` : ''}. Each item: {requirement: <exact acceptance text>, assertions: "<what the checks assert>", status?: "demonstrated"|"unverified"}.`);
|
|
51
61
|
if (params.coverage.some(c => c.status !== undefined && !['demonstrated', 'unverified'].includes(c.status))) throw Error('Coverage status must be demonstrated or unverified.');
|
|
52
62
|
if (!Array.isArray(params.probes) || !params.probes.length || params.probes.some(p => !nonempty(p))) throw Error('Record boundary/metamorphic probes and observed results, not just a generic review claim.');
|
|
53
63
|
if (!Array.isArray(params.findings) || params.findings.some(p => !nonempty(p))) throw Error('findings must be an array; unresolved findings require repairs or a blocked handoff.');
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"releaseVersion": "1.1.0-preview.
|
|
3
|
+
"releaseVersion": "1.1.0-preview.13",
|
|
4
4
|
"compatibilityVersion": "2.0.32",
|
|
5
5
|
"runtimeVersion": "0.0.0-ctg.2.0.32",
|
|
6
6
|
"sourceRevision": "843a6c514e606b436a7fe713422ace0a93ad3d61",
|
|
7
|
-
"patchSha256": "
|
|
7
|
+
"patchSha256": "ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975",
|
|
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": "d7c1869ace6dde4672f8ade4a8ba7826b15769a04b943571122d9541e52b62a7",
|
|
13
|
+
"executableSha256": "8a02fe556eb64def1507ff34a2cdd012dee636c0a6678f14d1fe57a020efa620"
|
|
14
14
|
},
|
|
15
15
|
"linux-arm64": {
|
|
16
16
|
"name": "opencode-linux-arm64.tar.gz",
|
|
17
|
-
"archiveSha256": "
|
|
18
|
-
"executableSha256": "
|
|
17
|
+
"archiveSha256": "7e5f7ea55e54a752d2923296f6917f7b48a175e5ae39a8caa540d844d89e6d8b",
|
|
18
|
+
"executableSha256": "eefc048de0b3932148050844b0d574c112bf13aeae001c767fac463dbbd1292e"
|
|
19
19
|
},
|
|
20
20
|
"darwin-x64": {
|
|
21
21
|
"name": "opencode-darwin-x64.tar.gz",
|
|
22
|
-
"archiveSha256": "
|
|
23
|
-
"executableSha256": "
|
|
22
|
+
"archiveSha256": "84be12c83c8cd9ef1028467fb53e94016e2b514013df9f62347a451f3c84044c",
|
|
23
|
+
"executableSha256": "4de7f310debebc00d955d8b4c349749b15bac69ef6998a051144d4e86b79b1a4"
|
|
24
24
|
},
|
|
25
25
|
"darwin-arm64": {
|
|
26
26
|
"name": "opencode-darwin-arm64.tar.gz",
|
|
27
|
-
"archiveSha256": "
|
|
28
|
-
"executableSha256": "
|
|
27
|
+
"archiveSha256": "8e9be7bda9df7a1fe84f1344738435f748ee56598c4635c2be8fdfef1433414a",
|
|
28
|
+
"executableSha256": "0963189d87daa88616980f72f3470b69902ac2b2b3b62cadb9e0fa436245b960"
|
|
29
29
|
},
|
|
30
30
|
"windows-x64": {
|
|
31
31
|
"name": "opencode-windows-x64.tar.gz",
|
|
32
|
-
"archiveSha256": "
|
|
33
|
-
"executableSha256": "
|
|
32
|
+
"archiveSha256": "5ca306190c354b5e6f690103da8a9db9e90bcf5463214f50cd1dfafd10109d4c",
|
|
33
|
+
"executableSha256": "4249c083b86b9bc034550954c2bc08ffdf9c578cf63c90a0adfabd103cac1594"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
package/lib/opencode/runtime.mjs
CHANGED
|
@@ -13,11 +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 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
|
|
17
17
|
// Update only this development pin after reviewing a new checked-in patch and
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
18
|
+
// script/ctg/prepare-development-runtime.sh. The published release pin above,
|
|
19
|
+
// its build recipe, and the release manifest stay immutable until a separately
|
|
20
|
+
// verified runtime release is promoted.
|
|
21
|
+
export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
|
|
21
22
|
const DEFAULT_ROOT = path.join(homedir(), '.carthagent', 'opencode');
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
@@ -64,11 +64,11 @@ const EXPERIENTIAL_MODEL_PROFILES = Object.freeze({
|
|
|
64
64
|
'glm-5.3-flash': { name: 'GLM 5.3 Flash', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text', 'image'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
|
|
65
65
|
'glm-5.3': { name: 'GLM 5.3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
|
|
66
66
|
'kimi-k3': { name: 'Kimi K3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsDeveloperRole: false, supportsReasoningEffort: true, thinkingFormat: 'deepseek' } },
|
|
67
|
-
'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true } },
|
|
67
|
+
'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, reasoningField: 'reasoning_content', requireReasoning: true, reasoningPlaceholder: ' ' } },
|
|
68
68
|
'mercury-3': { name: 'Mercury 3', reasoning: true, thinkingLevelMap: MERCURY3_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 131_072, input: ['text'], compat: { supportsReasoningEffort: true } },
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat'];
|
|
71
|
+
const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat', 'cost'];
|
|
72
72
|
|
|
73
73
|
export const EXPERIENTIAL_PROVIDER_ID = PROVIDER_ID;
|
|
74
74
|
export const EXPERIENTIAL_PROVIDER_NAME = CARTHAGENT_SHIP_PROVIDER_NAME;
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ export interface DailySourceReport {
|
|
|
8
8
|
freshness: 'current' | 'stale';
|
|
9
9
|
goal: string;
|
|
10
10
|
change: string;
|
|
11
|
-
checks: Array<{ id: string; before: 'not_run'; after: 'passed' | 'failed' | 'not_run'; revision: string; result: string }>;
|
|
11
|
+
checks: Array<{ id: string; before: 'not_run' | 'failed'; after: 'passed' | 'failed' | 'not_run'; revision: string; result: string }>;
|
|
12
12
|
/** Model-authored review/resolution claims, not independent verification. */
|
|
13
13
|
findings: Array<{ id: string; issue: string; evidence: string; resolution: string }>;
|
|
14
14
|
/** At least one field or item was omitted; the UI must warn before selection. */
|
|
@@ -20,7 +20,7 @@ const fingerprint = { type: 'string', minLength: 64, maxLength: 64 } as const;
|
|
|
20
20
|
const goal = { type: 'string', maxLength: 1024 } as const;
|
|
21
21
|
const check = {
|
|
22
22
|
type: 'object', properties: {
|
|
23
|
-
id, before: { type: 'string', enum: ['not_run'] },
|
|
23
|
+
id, before: { type: 'string', enum: ['not_run', 'failed'] },
|
|
24
24
|
after: { type: 'string', enum: ['passed', 'failed', 'not_run'] },
|
|
25
25
|
revision: fingerprint, result: goal,
|
|
26
26
|
}, required: ['id', 'before', 'after', 'revision', 'result'], additionalProperties: false,
|
|
@@ -57,6 +57,16 @@ function localJson(path: string, max = MAX_FILE): { value: unknown; raw: string
|
|
|
57
57
|
} catch (error) { if (missing(error)) return null; throw invalid(); }
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Receipts already hold an output tail; keep its end within the byte budget. */
|
|
61
|
+
function tail(value: unknown, limit: number, cut: () => void) {
|
|
62
|
+
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') <= limit) return value;
|
|
63
|
+
cut();
|
|
64
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
65
|
+
const kept = bytes.subarray(bytes.length - (limit - 4)).toString('utf8').replace(/^\uFFFD+/, '');
|
|
66
|
+
const line = kept.indexOf('\n');
|
|
67
|
+
return `…\n${line >= 0 && line < kept.length - 1 ? kept.slice(line + 1) : kept}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function validateState(value: unknown, directory: string, runId: string) {
|
|
61
71
|
if (!record(value) || value.runId !== runId || !Number.isSafeInteger(value.revision) || (value.revision as number) < 1 || !record(value.plan) || !Array.isArray(value.plan.checks) || value.plan.checks.length > 20000) throw invalid();
|
|
62
72
|
if (value.status !== undefined && !['implementing', 'verifying', 'verified', 'blocked'].includes(value.status as string)) throw invalid();
|
|
@@ -76,6 +86,7 @@ function validateState(value: unknown, directory: string, runId: string) {
|
|
|
76
86
|
}
|
|
77
87
|
if (value.reviewEvidence !== undefined && !record(value.reviewEvidence)) throw invalid();
|
|
78
88
|
if (value.handoff !== undefined && !record(value.handoff)) throw invalid();
|
|
89
|
+
if (value.baselines !== undefined && !record(value.baselines)) throw invalid();
|
|
79
90
|
if (value.reviewFindings !== undefined && (!Array.isArray(value.reviewFindings) || value.reviewFindings.length > 20000)) throw invalid();
|
|
80
91
|
const findings = (value.reviewFindings ?? []) as RecordValue[];
|
|
81
92
|
const findingIDs = new Set<string>();
|
|
@@ -129,9 +140,9 @@ function sourceFor(directory: string, sessionID: string): DailySourceReport | nu
|
|
|
129
140
|
const result: DailySourceReport = {
|
|
130
141
|
runId: parts[2], revision: current, planRevision: state.revision as number,
|
|
131
142
|
freshness: stale ? 'stale' : 'current', goal: text((state.plan as RecordValue).goal, 1024),
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
change: '',
|
|
143
|
+
// Only delivery_finish's explicit change summary for this snapshot; never
|
|
144
|
+
// derived from review/launch/limitations or the goal.
|
|
145
|
+
change: record(state.handoff) && state.handoff.fingerprint === current ? text(state.handoff.change, 3072) : '',
|
|
135
146
|
checks: checks.slice(0, 12).map(check => {
|
|
136
147
|
const receipt = evidence[check.id];
|
|
137
148
|
const fresh = Boolean(isFresh(check));
|
|
@@ -139,7 +150,10 @@ function sourceFor(directory: string, sessionID: string): DailySourceReport | nu
|
|
|
139
150
|
// Omit unbound failure output rather than making old/scope-error text look
|
|
140
151
|
// like an execution result for the current revision in the outbound DTO.
|
|
141
152
|
const output = receipt?.passed === false && !failed ? undefined : receipt?.outputTail;
|
|
142
|
-
|
|
153
|
+
// A first-in-run execution that failed on different source is a recorded before-failure.
|
|
154
|
+
const baseline = record(state.baselines) && record(state.baselines[check.id]) ? state.baselines[check.id] as RecordValue : undefined;
|
|
155
|
+
const before = baseline?.passed === false && baseline.checkDigest === checkDigest(check) && hash(baseline.fingerprint) && baseline.fingerprint !== current ? 'failed' as const : 'not_run' as const;
|
|
156
|
+
return { id: check.id, before, after: fresh ? 'passed' : failed ? 'failed' : 'not_run', revision: current, result: text(tail(output, 1024, () => { truncated = true; }), 1024) };
|
|
143
157
|
}),
|
|
144
158
|
findings: findings.slice(0, 5).map(finding => {
|
|
145
159
|
const resolution = record(finding.resolution) ? finding.resolution : {};
|
|
@@ -43,7 +43,7 @@ const short = S()
|
|
|
43
43
|
const steps = A({ anyOf: [short, O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, title: short, kind: { type: "string", enum: ["work", "regression"] }, dependsOn: A(short, 40), checks: A(short, 24) }, ["id", "title"])] }, 40)
|
|
44
44
|
const checks = A(O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, kind: { type: "string", enum: ["test", "runtime", "static"] }, argv: A(short, 80), timeoutSeconds: { type: "integer", minimum: 1, maximum: 300 } }), 24)
|
|
45
45
|
const acceptance = A(O({ requirement: short, checks: A(short, 24) }), 40)
|
|
46
|
-
const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(
|
|
46
|
+
const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(S("Relative file path that must exist at completion, e.g. invoice.mjs. Not a prose deliverable or command result; put test commands in checks. Omit outputs when no files are promised."), 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
|
|
47
47
|
|
|
48
48
|
function sessionKey(sessionID: string) { return `session/${sessionID}` }
|
|
49
49
|
|
|
@@ -149,8 +149,9 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
149
149
|
} finally { release() }
|
|
150
150
|
})
|
|
151
151
|
}
|
|
152
|
+
// The code-mode catalog drops the longest signatures first; pin the plan entry point so it is always listed.
|
|
152
153
|
const add = (editor: Parameters<Parameters<Context["tool"]["transform"]>[0]>[0], name: string, description: string, input: Schema, execute: (input: DeliveryInput, toolContext: RegisteredToolContext) => Promise<{ content: string }>) => editor.add({
|
|
153
|
-
name, description
|
|
154
|
+
name, description: `${description} Returns text; in code mode use JSON.parse(result) for JSON responses before reading fields. Plain-text messages are not JSON.`, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
|
|
154
155
|
const value = rawValue as DeliveryInput
|
|
155
156
|
const { active } = await scope(tool.sessionID)
|
|
156
157
|
if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
|
|
@@ -161,12 +162,12 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
161
162
|
return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
|
|
162
163
|
}
|
|
163
164
|
catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
|
|
164
|
-
}, options: { namespace: "carthagent", codemode: true },
|
|
165
|
+
}, options: { namespace: "carthagent", codemode: true, ...(name === "delivery_plan" ? { pinned: true } : {}) },
|
|
165
166
|
})
|
|
166
167
|
|
|
167
168
|
registrations.push(await ctx.tool.transform(editor => {
|
|
168
169
|
editor.namespace?.({ name: "carthagent", description: "Evidence-driven CTG delivery workflow tools" })
|
|
169
|
-
add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
|
|
170
|
+
add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Outputs are relative file paths, not prose or test results. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
|
|
170
171
|
if (data.configError) throw Error(data.configError)
|
|
171
172
|
const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
|
|
172
173
|
validateRevision(data.state, plan)
|
|
@@ -277,6 +278,9 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
277
278
|
}
|
|
278
279
|
const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
|
|
279
280
|
data.state.evidence[check.id] = { passed, ...identity, executedRevision: data.state.revision, fingerprint: after, code: result.code, timedOut: result.timedOut, cancelled: result.cancelled, logPath: result.logPath, durationMs: result.durationMs, changedDuringCheck: before !== after, outputTail: result.output.slice(-1200) }
|
|
281
|
+
// The first execution in a run is its baseline; later receipts replace evidence only.
|
|
282
|
+
const baselines = (data.state.baselines ??= {}) as Record<string, unknown>
|
|
283
|
+
if (!baselines[check.id]) baselines[check.id] = { passed, code: result.code, checkDigest: identity.checkDigest, fingerprint: after, at: new Date().toISOString() }
|
|
280
284
|
results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
|
|
281
285
|
await persist(cwd, tool.sessionID, data)
|
|
282
286
|
}
|
|
@@ -285,12 +289,12 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
|
|
|
285
289
|
const hash = fingerprint(cwd, ["."]); const issues = completionIssues(data.state, cwd, hash)
|
|
286
290
|
return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length), next: "Complete pending steps, review the evidence, then call delivery_finish." })
|
|
287
291
|
}))
|
|
288
|
-
add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
292
|
+
add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", properties: { requirement: { type: "string", description: "Exact acceptance requirement text" }, assertions: { anyOf: [{ type: "string" }, A({ type: "string" }, 32)], description: "Concrete assertions the linked checks make" }, assertion: { type: "string", description: "Alias for a single assertion" }, status: { type: "string", enum: ["demonstrated", "unverified"] } }, required: ["requirement"], additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
289
293
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
290
294
|
const result = input.action === "inspect" ? await captureDeliveryReview(data.state, cwd, runDirectory(cwd, tool.sessionID, data.state.runId), tool.signal) : input.action === "record" ? recordDeliveryReview(data.state, cwd, input) : (() => { throw Error("Review action must be inspect or record") })()
|
|
291
295
|
await persist(cwd, tool.sessionID, data); return text(result)
|
|
292
296
|
}))
|
|
293
|
-
add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
297
|
+
add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, change: { ...short, description: "One-paragraph summary of what changed and why" }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
|
|
294
298
|
if (!data.state) throw Error("Call delivery_plan first")
|
|
295
299
|
if (data.configError) throw Error(data.configError)
|
|
296
300
|
const verification = verificationMode(data.state.plan); let hash: string | null = null
|
|
@@ -98,7 +98,7 @@ function modelInfo(providerID: string, profile: CatalogModel, defaultBaseURL: st
|
|
|
98
98
|
|
|
99
99
|
function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.Info['compatibility']> {
|
|
100
100
|
const keys = [
|
|
101
|
-
'reasoningField', 'requireReasoning', 'maxTokensField', 'requireFinishReason',
|
|
101
|
+
'reasoningField', 'requireReasoning', 'reasoningPlaceholder', 'maxTokensField', 'requireFinishReason',
|
|
102
102
|
'requireAssistantAfterTool', 'supportsPromptCacheKey', 'cacheFormat',
|
|
103
103
|
'supportsThinkingBlockBinding', 'supportsEffortUpdates',
|
|
104
104
|
] as const;
|
|
@@ -8,6 +8,8 @@ import { unframe } from "./framing.mjs"
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
export const TOOL_SURFACE_GUIDANCE = "File edits and commands use the direct read/write/edit/bash tools. A code-mode search() catalog lists only namespaced tools.* helpers, so their absence there does not mean they are unavailable. If a tool is locked, its error names the required delivery step; take that step instead of searching for alternatives."
|
|
12
|
+
|
|
11
13
|
const browserCheck = fileURLToPath(new URL("../../../../tools/browser-check.mjs", import.meta.url))
|
|
12
14
|
const browserGuidance = existsSync(browserCheck)
|
|
13
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).`
|
|
@@ -24,8 +26,8 @@ export function shipGuidance({ state, workflow, prompt = "", profiles = [], disc
|
|
|
24
26
|
const task = originalTask(prompt)
|
|
25
27
|
const mode = state && !["verified", "blocked"].includes(state.status) ? state.workflowMode : workflow
|
|
26
28
|
const parts = mode === "standard"
|
|
27
|
-
? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE]
|
|
28
|
-
: [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state)]
|
|
29
|
+
? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE, TOOL_SURFACE_GUIDANCE]
|
|
30
|
+
: [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state), TOOL_SURFACE_GUIDANCE]
|
|
29
31
|
if (/\b(test|typescript|route|backend|api)\b/i.test(task)) parts.push(TEST_INFRASTRUCTURE_GUIDANCE)
|
|
30
32
|
if (profiles.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(task)) parts.push(browserGuidance)
|
|
31
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.")
|
|
@@ -17,6 +17,7 @@ import { VoyagesNavigation } from "./ui/voyages-navigation.js";
|
|
|
17
17
|
import { nativeNavigation } from "./ui/native-navigation.js";
|
|
18
18
|
import { provisionNativeThemes } from "./ui/theme-provision.mjs";
|
|
19
19
|
import { setSessionAutoCompaction } from "./ui/session-preferences.mjs";
|
|
20
|
+
import { runModelCommand } from "./ui/model-command.js";
|
|
20
21
|
import { createNativeReviewConsumer, nativeReviewMode, sendNativeReviewKickoff, subscribeNativeReviewEvents } from "./ui/native-review.mjs";
|
|
21
22
|
|
|
22
23
|
const THEMES = ["carthage", "tokyonight", "nebula", "ember", "forest", "mono", "obsidian", "midnight", "solarized-dark", "nord", "contrast-dark", "okabe-dark", "paper", "daylight", "solarized-light", "contrast-light", "okabe-light", "solarized", "okabe", "contrast", "system"];
|
|
@@ -45,8 +46,13 @@ export default Plugin.define({
|
|
|
45
46
|
else reviewLoops.delete(sessionID);
|
|
46
47
|
void updateReviewLoopState((state) => { state.sessions = [...reviewLoops]; });
|
|
47
48
|
},
|
|
49
|
+
// No formal delivery (unavailable) or a verified one; blocked/unfinished runs are not offered.
|
|
50
|
+
eligible: async (sessionID: string, signal?: AbortSignal) => {
|
|
51
|
+
const delivery = await workspaceClient(context, context.data.session.get(sessionID)?.location).delivery({ sessionID }, signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000));
|
|
52
|
+
return delivery.status === "unavailable" || delivery.status === "verified";
|
|
53
|
+
},
|
|
48
54
|
offer: async ({ sessionID, writes, start }: { sessionID: string; writes: number; start: () => Promise<boolean> }) => {
|
|
49
|
-
const accepted = await context.ui.dialog.confirm({ title: `Review ${context.data.session.get(sessionID)?.title ?? sessionID}?`, message: `This
|
|
55
|
+
const accepted = await context.ui.dialog.confirm({ title: `Review ${context.data.session.get(sessionID)?.title ?? sessionID}?`, message: `This run changed ${writes} file${writes === 1 ? "" : "s"}. Start the PR → fresh-context review → fixes workflow?`, label: { confirm: "Start review", cancel: "Not now" } });
|
|
50
56
|
if (accepted) await start();
|
|
51
57
|
},
|
|
52
58
|
kickoff: async (reviewSessionID: string, signal?: AbortSignal) => sendNativeReviewKickoff({ client: context.client, sessionID: reviewSessionID, signal }),
|
|
@@ -175,19 +181,17 @@ export default Plugin.define({
|
|
|
175
181
|
}
|
|
176
182
|
return;
|
|
177
183
|
}
|
|
178
|
-
case "model":
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} else if (selected) toast("Open or create a session before selecting its model.", "warning");
|
|
184
|
+
case "model":
|
|
185
|
+
await runModelCommand({
|
|
186
|
+
sync: () => context.data.location.model.sync(location),
|
|
187
|
+
list: () => context.data.location.model.list(location) ?? [],
|
|
188
|
+
select: (options) => context.ui.dialog.select({ title: "Select provider model", placeholder: "Search provider/model…", options }),
|
|
189
|
+
switchModel: (target, model) => context.client.session.switchModel({ sessionID: target, model }),
|
|
190
|
+
openNativePicker: (query) => context.keymap.dispatch("model.list", query),
|
|
191
|
+
providerName: nativeProviderName,
|
|
192
|
+
toast,
|
|
193
|
+
}, { sessionID, arg });
|
|
189
194
|
return;
|
|
190
|
-
}
|
|
191
195
|
case "thinking": {
|
|
192
196
|
if (!selectedModel) { toast("Choose a model first.", "warning"); return; }
|
|
193
197
|
const variants = context.ui.model.variant.list();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from "solid-js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import type { ScrollBoxRenderable, SelectRenderable, TextareaRenderable } from "@opentui/core";
|
|
5
5
|
import { usePlugin } from "@opencode/plugin/tui";
|
|
6
|
+
import { clickSelect } from "./select-mouse.js";
|
|
6
7
|
import { dailyQuestsClient, dailySourceClient } from "./client.js";
|
|
7
8
|
import type { DailyEvidence, DailyQuestDetail, DailySnapshot, DailySubmission, DailyRecord } from "../daily-quests-rpc.js";
|
|
8
9
|
import type { DailySourceReport } from "../daily-source-rpc.js";
|
|
@@ -192,6 +193,8 @@ export function DailyQuestsPage(props: { sessionID?: string; questID?: string })
|
|
|
192
193
|
// A navigator link selects only an assignment in the authenticated slate.
|
|
193
194
|
// It never composes evidence or opens a consent dialog.
|
|
194
195
|
void controller.refresh().then(() => { if (props.questID) return controller.select(props.questID); });
|
|
196
|
+
// The router reuses this page when another rail quest is opened.
|
|
197
|
+
createEffect(on(() => props.questID, (questID) => { if (questID) void controller.select(questID); }, { defer: true }));
|
|
195
198
|
onCleanup(() => { focus.dispose(); stop(); actions = undefined; controller.dispose(); closeOwnDialog(); });
|
|
196
199
|
});
|
|
197
200
|
return <box flexDirection="column" flexGrow={1} padding={1}>
|
|
@@ -227,6 +230,6 @@ export function DailyQuestsPage(props: { sessionID?: string; questID?: string })
|
|
|
227
230
|
<textarea ref={editor} flexBasis={0} flexGrow={1} minHeight={2} initialValue={view().draft?.text ?? ""} focused={focus.editorEnabled()} onContentChange={() => syncDailyEditor(editor, controller)} keyBindings={[{ name: "p", ctrl: true, action: "submit" }]} onSubmit={preview} />
|
|
228
231
|
</Show>
|
|
229
232
|
<text flexShrink={0} fg={context.theme.text.faint}>↑/↓ Enter · PgUp/Dn · Ctrl+U/D details · Cancel: no send</text>
|
|
230
|
-
<select ref={(node) => { actions = node; focus.attachActions(node); }} flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} showScrollIndicator options={options()} focused={focus.actionsEnabled()} keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} onSelect={(_index, option) => select(option?.value)} />
|
|
233
|
+
<select ref={(node) => { actions = node; focus.attachActions(node); }} onMouseUp={(event) => { if (focus.actionsEnabled()) clickSelect(actions, event); }} flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} showScrollIndicator options={options()} focused={focus.actionsEnabled()} keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} onSelect={(_index, option) => select(option?.value)} />
|
|
231
234
|
</box>;
|
|
232
235
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type CatalogModel = { providerID: string; modelID: string; id: string; name: string };
|
|
2
|
+
|
|
3
|
+
export type ModelCommandHost = {
|
|
4
|
+
sync(): Promise<unknown>;
|
|
5
|
+
list(): readonly CatalogModel[];
|
|
6
|
+
select(options: Array<{ title: string; value: CatalogModel; description: string }>): Promise<CatalogModel | undefined>;
|
|
7
|
+
switchModel(sessionID: string, model: { providerID: string; id: string }): Promise<unknown>;
|
|
8
|
+
/** Opens the native configured-model picker (the `/models` command) with an optional search. */
|
|
9
|
+
openNativePicker(query: string): void;
|
|
10
|
+
providerName(providerID: string): string;
|
|
11
|
+
toast(message: string, variant: "success" | "warning"): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `/model [name]`. Without a session the choice belongs to the next session's draft, which only the native
|
|
16
|
+
* picker owns, so it is opened directly; inside a session the selection is committed to that session.
|
|
17
|
+
*/
|
|
18
|
+
export async function runModelCommand(host: ModelCommandHost, input: { sessionID?: string; arg: string }) {
|
|
19
|
+
if (!input.sessionID) {
|
|
20
|
+
host.openNativePicker(input.arg);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await host.sync();
|
|
24
|
+
const query = input.arg.toLowerCase();
|
|
25
|
+
const options = host.list()
|
|
26
|
+
.filter((model) => !query || `${host.providerName(model.providerID)} ${model.providerID}/${model.modelID} ${model.name}`.toLowerCase().includes(query))
|
|
27
|
+
.map((model) => ({ title: `${host.providerName(model.providerID)}/${model.modelID}`, value: model, description: model.name }));
|
|
28
|
+
const selected = await host.select(options);
|
|
29
|
+
if (!selected) return;
|
|
30
|
+
await host.switchModel(input.sessionID, { providerID: selected.providerID, id: selected.id });
|
|
31
|
+
host.toast(`Selected ${host.providerName(selected.providerID)}/${selected.modelID}.`, "success");
|
|
32
|
+
}
|
|
@@ -10,6 +10,8 @@ export interface NativeReviewConsumerOptions {
|
|
|
10
10
|
diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
|
|
11
11
|
offer: (value: NativeReviewOffer) => Promise<unknown>;
|
|
12
12
|
kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
|
|
13
|
+
/** False or a rejection suppresses the offer (e.g. blocked formal delivery). */
|
|
14
|
+
eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>;
|
|
13
15
|
isLoopActive?: (sessionID: string) => boolean;
|
|
14
16
|
setLoopActive?: (sessionID: string, active: boolean) => void;
|
|
15
17
|
signal?: AbortSignal;
|