@cabane/companion 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -22
- package/dist/cli.js +137 -125
- package/dist/runtime.js +49 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Connect a **coding agent on your machine** to your [Cabane](https://cabane.ai) workspaces as a **responder** — it replies to messages inside Cabane while running as a full local AI client, so you can drive work against your own codebase, files, and MCP servers without putting any of it in Cabane. The model and tools run on your box; only the reply crosses back.
|
|
4
4
|
|
|
5
|
-
The Companion runs **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**
|
|
5
|
+
The Companion runs your agents through a **harness** on your machine — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See [Harnesses](#harnesses).
|
|
6
6
|
|
|
7
7
|
This is **v0**. It's a small CLI — install it from npm with one command, pair the machine once, and run. It talks to Cabane only over Cabane's public API.
|
|
8
8
|
|
|
@@ -16,9 +16,25 @@ The Companion runs on a **device you pair**, running **agents you assign to it**
|
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
|
-
You need **Node 22+** (the Companion
|
|
19
|
+
You need **Node 22+** (the Companion itself runs on Node — check with `node --version`) and **at least one harness installed and signed in**. The Companion installs no harness and drives no login for you — bring your own, and set it up **before** starting the Companion. It refuses to `start` if no harness is exposed, because no turn could be routed to it.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Set up whichever you already use — one is enough, and a machine can expose several. [Harnesses](#harnesses) has the full per-harness wiring; the short version:
|
|
22
|
+
|
|
23
|
+
**Claude Code** — install it globally _and_ log in:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm i -g @anthropic-ai/claude-code
|
|
27
|
+
claude # complete the login, then quit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Both halves are load-bearing, for different reasons:
|
|
31
|
+
|
|
32
|
+
- **The install** puts `claude` on your PATH (`which claude` should resolve). That PATH probe is how the Companion exposes Claude Code — it needs no configuration, but without `claude` the device won't offer it.
|
|
33
|
+
- **The login** writes the credential the turn actually uses. The Companion does _not_ run your global `claude` to answer a turn — inference goes through the bundled Claude Agent SDK — but the SDK reads the credential your Claude Code login left on disk. So a machine with `claude` installed and never logged in looks fine (the Connector reads **Online**) and then fails every turn with **"Sign-in needed"**.
|
|
34
|
+
|
|
35
|
+
**Codex** — install the Codex CLI and log in (`codex login`, or set `CODEX_API_KEY` in Codex's own environment — Cabane never sees the key). Unlike Claude Code, which is auto-detected on the PATH, Codex is an explicit opt-in: a one-line `codex` block in `~/.cabane/config.json`, or one click in the [dashboard](#the-dashboard). → [full setup](https://app.cabane.ai/docs/reference/connectors/chatgpt)
|
|
36
|
+
|
|
37
|
+
**opencode** — install it, authenticate a provider through its own flow, and start its server (`opencode serve`); the Companion addresses it at the URL you give it. → [full setup](https://app.cabane.ai/docs/reference/connectors/opencode)
|
|
22
38
|
|
|
23
39
|
You also need **a Cabane account.** A device is registered under your user account; the agents it runs can live in any workspace you have access to.
|
|
24
40
|
|
|
@@ -95,7 +111,7 @@ The prompt returns immediately and you can close the terminal — replies keep l
|
|
|
95
111
|
|
|
96
112
|
## The dashboard
|
|
97
113
|
|
|
98
|
-
`cabane-companion start` serves a small localhost dashboard (default `http://127.0.0.1:7474`) so the Companion is usable without tailing logs: live per-workspace connection status, the agents currently assigned to this device, a recent-dispatches feed, a log tail, and a few local-only controls (Stop / Restart / Reload-config). It binds to `127.0.0.1` only — same threat model as any localhost dev tool, no auth. It does **not** auto-open a browser by default — the URL is printed on start; pass `--open` (or set `autoOpen: true` in config) to open it automatically, and `--no-open` / `BRIDGE_NO_OPEN=1` force it off. It keeps working when Cabane itself is unreachable, showing a "⚠ Cabane unreachable" banner.
|
|
114
|
+
`cabane-companion start` serves a small localhost dashboard (default `http://127.0.0.1:7474`) so the Companion is usable without tailing logs: live per-workspace connection status, the agents currently assigned to this device, the [harnesses](#harnesses) it can see (with a one-click enable for one that's installed but not yet exposed), a recent-dispatches feed, a log tail, and a few local-only controls (Stop / Restart / Reload-config). It binds to `127.0.0.1` only — same threat model as any localhost dev tool, no auth. It does **not** auto-open a browser by default — the URL is printed on start; pass `--open` (or set `autoOpen: true` in config) to open it automatically, and `--no-open` / `BRIDGE_NO_OPEN=1` force it off. It keeps working when Cabane itself is unreachable, showing a "⚠ Cabane unreachable" banner.
|
|
99
115
|
|
|
100
116
|
## Configuring an agent: what's in Cabane vs. on this machine
|
|
101
117
|
|
|
@@ -163,34 +179,44 @@ When an agent's MCP servers in Cabane need a credential, that credential is writ
|
|
|
163
179
|
|
|
164
180
|
A `${VAR}` the store doesn't declare fails the turn loudly — it is **never** read from your shell environment, so a config can't smuggle out an ambient credential. The Companion reports the declared **names** (never values) to Cabane on its heartbeat, so the settings UI can warn "this agent needs `${GITHUB_TOKEN}`, this device doesn't expose it" before a turn ever runs.
|
|
165
181
|
|
|
166
|
-
##
|
|
182
|
+
## Harnesses
|
|
183
|
+
|
|
184
|
+
A **harness** is the local tool that runs a model — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — together with the credential behind it. A harness connected to Cabane on this device is a **Connector**, and one device runs as many Connectors as it has harnesses. None is privileged: a device advertises exactly the harnesses it actually has.
|
|
167
185
|
|
|
168
|
-
|
|
186
|
+
Which harness a turn uses follows the agent's **model**, set in Cabane (Workspace settings → Agents): a Claude model runs on Claude Code, an OpenAI model on Codex, an opencode model on opencode. A device only offers a harness's models — and only accepts its turns — once that harness is exposed, so set it up first, then pick a matching model for the agent. A turn is never routed to a harness the machine can't run.
|
|
169
187
|
|
|
170
|
-
|
|
188
|
+
What "exposed" means differs per harness, because each is discoverable in a different way. Bring your own install and your own login in all three cases — the Companion never installs a binary and never drives a login:
|
|
171
189
|
|
|
172
|
-
|
|
190
|
+
| Harness | How the device exposes it | Setup |
|
|
191
|
+
| --------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
192
|
+
| **Claude Code** | `claude` is on your PATH — auto-detected, no configuration. | [Claude](https://app.cabane.ai/docs/reference/connectors/claude) |
|
|
193
|
+
| **Codex** | A `codex` block in `~/.cabane/config.json`. Unlike Claude Code, an explicit opt-in. | [ChatGPT (Codex)](https://app.cabane.ai/docs/reference/connectors/chatgpt) |
|
|
194
|
+
| **opencode** | An `opencode.serverUrl` in `~/.cabane/config.json` pointing at a reachable `opencode serve`. | [opencode](https://app.cabane.ai/docs/reference/connectors/opencode) |
|
|
195
|
+
|
|
196
|
+
Each harness has a full setup page in the docs — install, login, enable, verify — linked above; this section is just the shape of it and the config the Companion reads.
|
|
197
|
+
|
|
198
|
+
**You usually don't hand-edit the config.** `cabane-companion start` serves a [localhost dashboard](#the-dashboard) listing every harness it can see, including ones installed but not yet exposed: a `codex` binary on your PATH shows up as detected-but-not-enabled with a **Use Codex** button, and opencode gets an **Add opencode** field for the `serve` URL (health-checked before it's written, so an unreachable server is caught in place). One click writes the block and advertises the Connector straight away — no restart. Editing `~/.cabane/config.json` yourself is the fallback, and what a headless device wants:
|
|
199
|
+
|
|
200
|
+
```jsonc
|
|
201
|
+
{
|
|
202
|
+
// …device identity, managed by `pair`; don't hand-edit those fields…
|
|
203
|
+
"codex": { "enabled": true },
|
|
204
|
+
"opencode": { "serverUrl": "http://127.0.0.1:4096" },
|
|
205
|
+
}
|
|
206
|
+
```
|
|
173
207
|
|
|
174
|
-
|
|
175
|
-
2. **Start its server:** `opencode serve --port 4096` (any free port). Run one `opencode serve` per Companion process.
|
|
176
|
-
3. **Point the Companion at it** — add a top-level `opencode` block to `~/.cabane/config.json`:
|
|
177
|
-
```jsonc
|
|
178
|
-
{
|
|
179
|
-
// …device identity, managed by `pair`; don't hand-edit those fields…
|
|
180
|
-
"opencode": { "serverUrl": "http://127.0.0.1:4096" },
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
4. **Restart the Companion.** Its next heartbeat advertises the `opencode` runtime, and an agent assigned to this device can now be set to an opencode model in Cabane. Without this block the device stays Claude-Code-only, exactly as before.
|
|
208
|
+
Either block is optional and independent — set only the harnesses you have. Codex is a local CLI the bundled `@openai/codex-sdk` spawns per turn, so it takes a flag and no URL; opencode is a long-lived server addressed by URL, so run one `opencode serve` per Companion process. A hand-edit needs a restart (`cabane-companion stop && cabane-companion start`); the dashboard's button doesn't.
|
|
184
209
|
|
|
185
|
-
Everything else about a Companion-run agent is
|
|
210
|
+
Everything else about a Companion-run agent is the same whichever harness runs it — same assignment, same working directory, same secrets, same dashboard. A harness is a way to execute a turn, not a different way to run the Companion.
|
|
186
211
|
|
|
187
212
|
## Files on disk
|
|
188
213
|
|
|
189
214
|
```
|
|
190
215
|
~/.cabane/
|
|
191
216
|
├── config.json # device identity: baseUrl + device token (cabdev_…) + device id/label,
|
|
192
|
-
│ # plus optional per-agent `agents` overrides (cwd / prepareHook),
|
|
193
|
-
│ # optional `opencode`
|
|
217
|
+
│ # plus optional per-agent `agents` overrides (cwd / prepareHook), the
|
|
218
|
+
│ # optional harness blocks (`codex` enabled, `opencode` serverUrl), and
|
|
219
|
+
│ # dashboard prefs. mode 600.
|
|
194
220
|
│ # No account password, no full-account token.
|
|
195
221
|
├── credentials.json # agentId → per-agent workspace-bound token, cached on first assignment
|
|
196
222
|
│ # pull (delivered once, then never re-sent). mode 600.
|
|
@@ -206,7 +232,7 @@ No SQLite, no embedded DB. Cabane is the source of truth for which agents to run
|
|
|
206
232
|
|
|
207
233
|
## Debugging a turn: transcripts
|
|
208
234
|
|
|
209
|
-
The dashboard shows the agent's final reply, but not _how_ it got there. When a turn misbehaves — an agent that "couldn't read the file", a tool that errored, MCP tools that didn't load, a missing secret — the full picture is on disk. Every dispatch writes the complete
|
|
235
|
+
The dashboard shows the agent's final reply, but not _how_ it got there. When a turn misbehaves — an agent that "couldn't read the file", a tool that errored, MCP tools that didn't load, a missing secret — the full picture is on disk. Every dispatch — on whichever harness ran it — writes the complete turn stream (the system init with its tool list, every tool call **and its result**, the assistant text, the outcome) to a JSONL file under `~/.cabane/transcripts/`. On a failed turn, the Companion log also prints the exact path.
|
|
210
236
|
|
|
211
237
|
Read one back in a readable form:
|
|
212
238
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/daemon.ts
|
|
7
|
-
import { spawn as
|
|
7
|
+
import { spawn as spawn4 } from "child_process";
|
|
8
8
|
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2 } from "fs";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
|
|
@@ -457,11 +457,94 @@ function getLogger() {
|
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
// src/prereqs.ts
|
|
460
|
+
import { spawn as spawn3 } from "child_process";
|
|
461
|
+
|
|
462
|
+
// src/harness-versions.ts
|
|
460
463
|
import { spawn as spawn2 } from "child_process";
|
|
464
|
+
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
465
|
+
function parseVersionToken(raw) {
|
|
466
|
+
if (!raw) return null;
|
|
467
|
+
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
468
|
+
return m ? m[0] : null;
|
|
469
|
+
}
|
|
470
|
+
async function probeCliVersion(command, spawnImpl = spawn2) {
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
let settled = false;
|
|
473
|
+
const done = (v) => {
|
|
474
|
+
if (!settled) {
|
|
475
|
+
settled = true;
|
|
476
|
+
resolve(v);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
let child;
|
|
480
|
+
try {
|
|
481
|
+
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
482
|
+
} catch {
|
|
483
|
+
done(null);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
let out = "";
|
|
487
|
+
child.stdout?.on("data", (chunk) => {
|
|
488
|
+
if (out.length < 4096) out += chunk.toString();
|
|
489
|
+
});
|
|
490
|
+
child.once("error", () => done(null));
|
|
491
|
+
child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
495
|
+
try {
|
|
496
|
+
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
497
|
+
const res = await fetchImpl(`${base}/global/health`, {
|
|
498
|
+
headers: { accept: "application/json" }
|
|
499
|
+
});
|
|
500
|
+
if (!res.ok) return null;
|
|
501
|
+
return extractOpencodeVersion(await res.json());
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function extractOpencodeVersion(json) {
|
|
507
|
+
if (!json || typeof json !== "object") return null;
|
|
508
|
+
const obj = json;
|
|
509
|
+
const candidates = [obj.version];
|
|
510
|
+
for (const v of Object.values(obj)) {
|
|
511
|
+
if (v && typeof v === "object") candidates.push(v.version);
|
|
512
|
+
}
|
|
513
|
+
for (const c of candidates) {
|
|
514
|
+
if (typeof c === "string") {
|
|
515
|
+
const parsed = parseVersionToken(c);
|
|
516
|
+
if (parsed) return parsed;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
async function probeHarnessVersions(opts, deps = {}) {
|
|
522
|
+
const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
|
|
523
|
+
const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
|
|
524
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
525
|
+
const [claudeCode, codex, opencode] = await Promise.all([
|
|
526
|
+
opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
|
|
527
|
+
opts.codex ? safe(probeCodex) : Promise.resolve(null),
|
|
528
|
+
opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
|
|
529
|
+
]);
|
|
530
|
+
return { claudeCode, opencode, codex };
|
|
531
|
+
}
|
|
532
|
+
function emptyHarnessVersions() {
|
|
533
|
+
return { ...EMPTY };
|
|
534
|
+
}
|
|
535
|
+
async function safe(fn) {
|
|
536
|
+
try {
|
|
537
|
+
return await fn();
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/prereqs.ts
|
|
461
544
|
async function claudeOnPath() {
|
|
462
545
|
return new Promise((resolve) => {
|
|
463
546
|
let settled = false;
|
|
464
|
-
const child =
|
|
547
|
+
const child = spawn3("claude", ["--version"], { stdio: "ignore" });
|
|
465
548
|
child.once("error", () => {
|
|
466
549
|
if (!settled) {
|
|
467
550
|
settled = true;
|
|
@@ -476,8 +559,20 @@ async function claudeOnPath() {
|
|
|
476
559
|
});
|
|
477
560
|
});
|
|
478
561
|
}
|
|
562
|
+
var CODEX_PROBE_TIMEOUT_MS = 4e3;
|
|
563
|
+
async function codexOnPath() {
|
|
564
|
+
const version = await Promise.race([
|
|
565
|
+
probeCliVersion("codex"),
|
|
566
|
+
new Promise((resolve) => {
|
|
567
|
+
const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
|
|
568
|
+
timer.unref?.();
|
|
569
|
+
})
|
|
570
|
+
]);
|
|
571
|
+
return version !== null;
|
|
572
|
+
}
|
|
479
573
|
async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
480
574
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
575
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
481
576
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
482
577
|
`));
|
|
483
578
|
if (await probeClaude()) return;
|
|
@@ -486,15 +581,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
|
486
581
|
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
487
582
|
];
|
|
488
583
|
if (alternates.length > 0) {
|
|
584
|
+
const exposed = alternates.join(" + ");
|
|
489
585
|
warn(
|
|
490
|
-
`warning: Claude Code isn\u2019t on your PATH, so this device
|
|
491
|
-
" + "
|
|
492
|
-
)} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
|
|
586
|
+
`warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
|
|
493
587
|
);
|
|
494
588
|
return;
|
|
495
589
|
}
|
|
590
|
+
const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
|
|
496
591
|
throw new CompanionError(
|
|
497
|
-
"
|
|
592
|
+
"This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
|
|
498
593
|
);
|
|
499
594
|
}
|
|
500
595
|
|
|
@@ -648,7 +743,7 @@ function defaultSpawnDetached(args) {
|
|
|
648
743
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
649
744
|
const logFd = openSync2(companionLogPath(), "a");
|
|
650
745
|
try {
|
|
651
|
-
return
|
|
746
|
+
return spawn4(process.execPath, [cliPath, ...args], {
|
|
652
747
|
detached: true,
|
|
653
748
|
stdio: ["ignore", logFd, logFd],
|
|
654
749
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -922,12 +1017,12 @@ async function pair(opts = {}) {
|
|
|
922
1017
|
import { readFileSync as readFileSync11 } from "fs";
|
|
923
1018
|
|
|
924
1019
|
// src/browser.ts
|
|
925
|
-
import { spawn as
|
|
1020
|
+
import { spawn as spawn5 } from "child_process";
|
|
926
1021
|
import { platform } from "process";
|
|
927
1022
|
function openBrowser(url) {
|
|
928
1023
|
try {
|
|
929
1024
|
const { command, args } = openerFor(url);
|
|
930
|
-
const child =
|
|
1025
|
+
const child = spawn5(command, args, { stdio: "ignore", detached: true });
|
|
931
1026
|
child.on("error", () => {
|
|
932
1027
|
});
|
|
933
1028
|
child.unref();
|
|
@@ -1434,87 +1529,6 @@ function resolveStaticDir() {
|
|
|
1434
1529
|
return join6(dirname4(fileURLToPath2(import.meta.url)), "static");
|
|
1435
1530
|
}
|
|
1436
1531
|
|
|
1437
|
-
// src/harness-versions.ts
|
|
1438
|
-
import { spawn as spawn5 } from "child_process";
|
|
1439
|
-
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
1440
|
-
function parseVersionToken(raw) {
|
|
1441
|
-
if (!raw) return null;
|
|
1442
|
-
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
1443
|
-
return m ? m[0] : null;
|
|
1444
|
-
}
|
|
1445
|
-
async function probeCliVersion(command, spawnImpl = spawn5) {
|
|
1446
|
-
return new Promise((resolve) => {
|
|
1447
|
-
let settled = false;
|
|
1448
|
-
const done = (v) => {
|
|
1449
|
-
if (!settled) {
|
|
1450
|
-
settled = true;
|
|
1451
|
-
resolve(v);
|
|
1452
|
-
}
|
|
1453
|
-
};
|
|
1454
|
-
let child;
|
|
1455
|
-
try {
|
|
1456
|
-
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1457
|
-
} catch {
|
|
1458
|
-
done(null);
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
let out = "";
|
|
1462
|
-
child.stdout?.on("data", (chunk) => {
|
|
1463
|
-
if (out.length < 4096) out += chunk.toString();
|
|
1464
|
-
});
|
|
1465
|
-
child.once("error", () => done(null));
|
|
1466
|
-
child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
1470
|
-
try {
|
|
1471
|
-
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
1472
|
-
const res = await fetchImpl(`${base}/global/health`, {
|
|
1473
|
-
headers: { accept: "application/json" }
|
|
1474
|
-
});
|
|
1475
|
-
if (!res.ok) return null;
|
|
1476
|
-
return extractOpencodeVersion(await res.json());
|
|
1477
|
-
} catch {
|
|
1478
|
-
return null;
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
function extractOpencodeVersion(json) {
|
|
1482
|
-
if (!json || typeof json !== "object") return null;
|
|
1483
|
-
const obj = json;
|
|
1484
|
-
const candidates = [obj.version];
|
|
1485
|
-
for (const v of Object.values(obj)) {
|
|
1486
|
-
if (v && typeof v === "object") candidates.push(v.version);
|
|
1487
|
-
}
|
|
1488
|
-
for (const c of candidates) {
|
|
1489
|
-
if (typeof c === "string") {
|
|
1490
|
-
const parsed = parseVersionToken(c);
|
|
1491
|
-
if (parsed) return parsed;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
async function probeHarnessVersions(opts, deps = {}) {
|
|
1497
|
-
const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
|
|
1498
|
-
const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
|
|
1499
|
-
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1500
|
-
const [claudeCode, codex, opencode] = await Promise.all([
|
|
1501
|
-
opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
|
|
1502
|
-
opts.codex ? safe(probeCodex) : Promise.resolve(null),
|
|
1503
|
-
opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
|
|
1504
|
-
]);
|
|
1505
|
-
return { claudeCode, opencode, codex };
|
|
1506
|
-
}
|
|
1507
|
-
function emptyHarnessVersions() {
|
|
1508
|
-
return { ...EMPTY };
|
|
1509
|
-
}
|
|
1510
|
-
async function safe(fn) {
|
|
1511
|
-
try {
|
|
1512
|
-
return await fn();
|
|
1513
|
-
} catch {
|
|
1514
|
-
return null;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
1532
|
// src/api.ts
|
|
1519
1533
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
1520
1534
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
@@ -2972,6 +2986,11 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2972
2986
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
2973
2987
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
2974
2988
|
const model = parseClaudeCodeModel(config.model);
|
|
2989
|
+
if (model === null) {
|
|
2990
|
+
console.warn(
|
|
2991
|
+
`[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2975
2994
|
const base = {
|
|
2976
2995
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
2977
2996
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -3593,26 +3612,20 @@ function selectAdapter(registry, runtime) {
|
|
|
3593
3612
|
|
|
3594
3613
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
3595
3614
|
var OPENCODE_ADDENDUM = [
|
|
3596
|
-
"Your Cabane
|
|
3597
|
-
"
|
|
3598
|
-
"
|
|
3615
|
+
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3616
|
+
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3617
|
+
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3618
|
+
"plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
|
|
3619
|
+
"`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
3620
|
+
"`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
|
|
3621
|
+
"`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
|
|
3622
|
+
"`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
|
|
3623
|
+
"HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
|
|
3599
3624
|
"prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
|
|
3600
3625
|
"closing reply as the last thing you say in the turn: you can interleave",
|
|
3601
3626
|
"narration with tool calls, but only your final message is recorded as the",
|
|
3602
3627
|
"turn\u2019s reply."
|
|
3603
3628
|
].join(" ");
|
|
3604
|
-
var OPENCODE_ADDENDUM_CODE_MODE = [
|
|
3605
|
-
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3606
|
-
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3607
|
-
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3608
|
-
"plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
|
|
3609
|
-
"There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
|
|
3610
|
-
"calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
|
|
3611
|
-
"a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
|
|
3612
|
-
"of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
|
|
3613
|
-
"last thing you say in the turn: you can interleave narration with tool calls, but",
|
|
3614
|
-
"only your final message is recorded as the turn\u2019s reply."
|
|
3615
|
-
].join(" ");
|
|
3616
3629
|
|
|
3617
3630
|
// packages/agent-runtime/src/opencode/events.ts
|
|
3618
3631
|
function asRecord(v) {
|
|
@@ -4164,7 +4177,7 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4164
4177
|
name: "opencode",
|
|
4165
4178
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
4166
4179
|
// `code`, not the granular cabane names it no longer has.
|
|
4167
|
-
promptAddendum: (
|
|
4180
|
+
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
4168
4181
|
dialectSchema: opencodeDialectSchema,
|
|
4169
4182
|
async *runTurn(req, signal) {
|
|
4170
4183
|
if (!transport) {
|
|
@@ -4564,14 +4577,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
4564
4577
|
|
|
4565
4578
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
4566
4579
|
var CODEX_ADDENDUM = [
|
|
4567
|
-
"Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
|
|
4568
|
-
"`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
|
|
4569
|
-
"prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
|
|
4570
|
-
"Write your closing reply as the last thing you say in the turn: you can",
|
|
4571
|
-
"interleave narration with tool calls, but only your final message is recorded",
|
|
4572
|
-
"as the turn\u2019s reply."
|
|
4573
|
-
].join(" ");
|
|
4574
|
-
var CODEX_ADDENDUM_CODE_MODE = [
|
|
4575
4580
|
"Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
|
|
4576
4581
|
"`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
|
|
4577
4582
|
"`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
|
|
@@ -4580,13 +4585,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
|
|
|
4580
4585
|
"declare the SDK absent without attempting discovery and invocation. The SDK",
|
|
4581
4586
|
"call runs a TypeScript program against the ambient `cabane` object. The",
|
|
4582
4587
|
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
4583
|
-
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too)
|
|
4584
|
-
"
|
|
4585
|
-
"
|
|
4586
|
-
"
|
|
4587
|
-
"
|
|
4588
|
-
"
|
|
4589
|
-
"
|
|
4588
|
+
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
|
|
4589
|
+
"the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
|
|
4590
|
+
"`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
|
|
4591
|
+
"There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
|
|
4592
|
+
"SDK calls inside your program, not tools. If a tool appears in this prompt with",
|
|
4593
|
+
"an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
|
|
4594
|
+
"the last thing you say in the turn: you can interleave narration with tool",
|
|
4595
|
+
"calls, but only your final message is recorded as the turn\u2019s reply."
|
|
4590
4596
|
].join(" ");
|
|
4591
4597
|
|
|
4592
4598
|
// packages/agent-runtime/src/codex/events.ts
|
|
@@ -4895,10 +4901,16 @@ function buildRunSpec2(req, resumeThreadId) {
|
|
|
4895
4901
|
const { policy, config } = req;
|
|
4896
4902
|
const directory = req.local.cwd ?? "";
|
|
4897
4903
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
4904
|
+
const model = config.model ? parseCodexModel(config.model) : null;
|
|
4905
|
+
if (model === null) {
|
|
4906
|
+
console.warn(
|
|
4907
|
+
`[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
4898
4910
|
return {
|
|
4899
4911
|
resumeThreadId,
|
|
4900
4912
|
directory,
|
|
4901
|
-
model
|
|
4913
|
+
model,
|
|
4902
4914
|
policy: codexToolPolicy(policy),
|
|
4903
4915
|
skipGitRepoCheck: true,
|
|
4904
4916
|
...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
|
|
@@ -5566,7 +5578,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
5566
5578
|
name: "codex",
|
|
5567
5579
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
5568
5580
|
// `code`, not the granular names it no longer has.
|
|
5569
|
-
promptAddendum: (
|
|
5581
|
+
promptAddendum: () => CODEX_ADDENDUM,
|
|
5570
5582
|
dialectSchema: codexDialectSchema,
|
|
5571
5583
|
async *runTurn(req, signal) {
|
|
5572
5584
|
if (!transport) {
|
|
@@ -7868,8 +7880,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
7868
7880
|
return {
|
|
7869
7881
|
claudeOnPath: claudeOnPathResult,
|
|
7870
7882
|
claudeVersion,
|
|
7871
|
-
// A parseable `codex --version` is our presence signal (
|
|
7872
|
-
//
|
|
7883
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7884
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
7873
7885
|
codexOnPath: codexVersion !== null,
|
|
7874
7886
|
codexVersion,
|
|
7875
7887
|
codexEnabled: isCodexEnabled(cfg),
|
package/dist/runtime.js
CHANGED
|
@@ -979,8 +979,20 @@ async function claudeOnPath() {
|
|
|
979
979
|
});
|
|
980
980
|
});
|
|
981
981
|
}
|
|
982
|
+
var CODEX_PROBE_TIMEOUT_MS = 4e3;
|
|
983
|
+
async function codexOnPath() {
|
|
984
|
+
const version = await Promise.race([
|
|
985
|
+
probeCliVersion("codex"),
|
|
986
|
+
new Promise((resolve) => {
|
|
987
|
+
const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
|
|
988
|
+
timer.unref?.();
|
|
989
|
+
})
|
|
990
|
+
]);
|
|
991
|
+
return version !== null;
|
|
992
|
+
}
|
|
982
993
|
async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
983
994
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
995
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
984
996
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
985
997
|
`));
|
|
986
998
|
if (await probeClaude()) return;
|
|
@@ -989,15 +1001,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
|
989
1001
|
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
990
1002
|
];
|
|
991
1003
|
if (alternates.length > 0) {
|
|
1004
|
+
const exposed = alternates.join(" + ");
|
|
992
1005
|
warn(
|
|
993
|
-
`warning: Claude Code isn\u2019t on your PATH, so this device
|
|
994
|
-
" + "
|
|
995
|
-
)} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
|
|
1006
|
+
`warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
|
|
996
1007
|
);
|
|
997
1008
|
return;
|
|
998
1009
|
}
|
|
1010
|
+
const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
|
|
999
1011
|
throw new CompanionError(
|
|
1000
|
-
"
|
|
1012
|
+
"This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
|
|
1001
1013
|
);
|
|
1002
1014
|
}
|
|
1003
1015
|
|
|
@@ -2627,6 +2639,11 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2627
2639
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
2628
2640
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
2629
2641
|
const model = parseClaudeCodeModel(config.model);
|
|
2642
|
+
if (model === null) {
|
|
2643
|
+
console.warn(
|
|
2644
|
+
`[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2630
2647
|
const base = {
|
|
2631
2648
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
2632
2649
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -3248,26 +3265,20 @@ function selectAdapter(registry, runtime) {
|
|
|
3248
3265
|
|
|
3249
3266
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
3250
3267
|
var OPENCODE_ADDENDUM = [
|
|
3251
|
-
"Your Cabane
|
|
3252
|
-
"
|
|
3253
|
-
"
|
|
3268
|
+
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3269
|
+
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3270
|
+
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3271
|
+
"plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
|
|
3272
|
+
"`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
3273
|
+
"`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
|
|
3274
|
+
"`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
|
|
3275
|
+
"`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
|
|
3276
|
+
"HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
|
|
3254
3277
|
"prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
|
|
3255
3278
|
"closing reply as the last thing you say in the turn: you can interleave",
|
|
3256
3279
|
"narration with tool calls, but only your final message is recorded as the",
|
|
3257
3280
|
"turn\u2019s reply."
|
|
3258
3281
|
].join(" ");
|
|
3259
|
-
var OPENCODE_ADDENDUM_CODE_MODE = [
|
|
3260
|
-
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3261
|
-
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3262
|
-
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3263
|
-
"plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
|
|
3264
|
-
"There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
|
|
3265
|
-
"calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
|
|
3266
|
-
"a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
|
|
3267
|
-
"of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
|
|
3268
|
-
"last thing you say in the turn: you can interleave narration with tool calls, but",
|
|
3269
|
-
"only your final message is recorded as the turn\u2019s reply."
|
|
3270
|
-
].join(" ");
|
|
3271
3282
|
|
|
3272
3283
|
// packages/agent-runtime/src/opencode/events.ts
|
|
3273
3284
|
function asRecord(v) {
|
|
@@ -3819,7 +3830,7 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
3819
3830
|
name: "opencode",
|
|
3820
3831
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
3821
3832
|
// `code`, not the granular cabane names it no longer has.
|
|
3822
|
-
promptAddendum: (
|
|
3833
|
+
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
3823
3834
|
dialectSchema: opencodeDialectSchema,
|
|
3824
3835
|
async *runTurn(req, signal) {
|
|
3825
3836
|
if (!transport) {
|
|
@@ -4219,14 +4230,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
4219
4230
|
|
|
4220
4231
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
4221
4232
|
var CODEX_ADDENDUM = [
|
|
4222
|
-
"Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
|
|
4223
|
-
"`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
|
|
4224
|
-
"prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
|
|
4225
|
-
"Write your closing reply as the last thing you say in the turn: you can",
|
|
4226
|
-
"interleave narration with tool calls, but only your final message is recorded",
|
|
4227
|
-
"as the turn\u2019s reply."
|
|
4228
|
-
].join(" ");
|
|
4229
|
-
var CODEX_ADDENDUM_CODE_MODE = [
|
|
4230
4233
|
"Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
|
|
4231
4234
|
"`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
|
|
4232
4235
|
"`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
|
|
@@ -4235,13 +4238,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
|
|
|
4235
4238
|
"declare the SDK absent without attempting discovery and invocation. The SDK",
|
|
4236
4239
|
"call runs a TypeScript program against the ambient `cabane` object. The",
|
|
4237
4240
|
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
4238
|
-
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too)
|
|
4239
|
-
"
|
|
4240
|
-
"
|
|
4241
|
-
"
|
|
4242
|
-
"
|
|
4243
|
-
"
|
|
4244
|
-
"
|
|
4241
|
+
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
|
|
4242
|
+
"the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
|
|
4243
|
+
"`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
|
|
4244
|
+
"There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
|
|
4245
|
+
"SDK calls inside your program, not tools. If a tool appears in this prompt with",
|
|
4246
|
+
"an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
|
|
4247
|
+
"the last thing you say in the turn: you can interleave narration with tool",
|
|
4248
|
+
"calls, but only your final message is recorded as the turn\u2019s reply."
|
|
4245
4249
|
].join(" ");
|
|
4246
4250
|
|
|
4247
4251
|
// packages/agent-runtime/src/codex/events.ts
|
|
@@ -4550,10 +4554,16 @@ function buildRunSpec2(req, resumeThreadId) {
|
|
|
4550
4554
|
const { policy, config } = req;
|
|
4551
4555
|
const directory = req.local.cwd ?? "";
|
|
4552
4556
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
4557
|
+
const model = config.model ? parseCodexModel(config.model) : null;
|
|
4558
|
+
if (model === null) {
|
|
4559
|
+
console.warn(
|
|
4560
|
+
`[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
4561
|
+
);
|
|
4562
|
+
}
|
|
4553
4563
|
return {
|
|
4554
4564
|
resumeThreadId,
|
|
4555
4565
|
directory,
|
|
4556
|
-
model
|
|
4566
|
+
model,
|
|
4557
4567
|
policy: codexToolPolicy(policy),
|
|
4558
4568
|
skipGitRepoCheck: true,
|
|
4559
4569
|
...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
|
|
@@ -5221,7 +5231,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
5221
5231
|
name: "codex",
|
|
5222
5232
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
5223
5233
|
// `code`, not the granular names it no longer has.
|
|
5224
|
-
promptAddendum: (
|
|
5234
|
+
promptAddendum: () => CODEX_ADDENDUM,
|
|
5225
5235
|
dialectSchema: codexDialectSchema,
|
|
5226
5236
|
async *runTurn(req, signal) {
|
|
5227
5237
|
if (!transport) {
|
|
@@ -7523,8 +7533,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
7523
7533
|
return {
|
|
7524
7534
|
claudeOnPath: claudeOnPathResult,
|
|
7525
7535
|
claudeVersion,
|
|
7526
|
-
// A parseable `codex --version` is our presence signal (
|
|
7527
|
-
//
|
|
7536
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7537
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
7528
7538
|
codexOnPath: codexVersion !== null,
|
|
7529
7539
|
codexVersion,
|
|
7530
7540
|
codexEnabled: isCodexEnabled(cfg),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|