@cabane/companion 0.6.89 → 0.6.91
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 +17 -11
- package/dist/cli.js +675 -215
- package/dist/pairing-config.js +9 -6
- package/dist/postinstall.js +209 -0
- package/dist/runtime.js +664 -204
- package/package.json +2 -1
package/dist/runtime.js
CHANGED
|
@@ -120,7 +120,7 @@ function parsePrepareOutput(stdout) {
|
|
|
120
120
|
return { cwd: last };
|
|
121
121
|
}
|
|
122
122
|
var runPrepareHook = (hook, input) => {
|
|
123
|
-
return new Promise((
|
|
123
|
+
return new Promise((resolve2, reject) => {
|
|
124
124
|
const timeoutMs = hook.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
125
125
|
let settled = false;
|
|
126
126
|
const finish = (fn) => {
|
|
@@ -194,11 +194,11 @@ var runPrepareHook = (hook, input) => {
|
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
if (input.prepared) {
|
|
197
|
-
|
|
197
|
+
resolve2(input.prepared);
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
try {
|
|
201
|
-
|
|
201
|
+
resolve2(parsePrepareOutput(stdout));
|
|
202
202
|
} catch (err) {
|
|
203
203
|
reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
|
|
204
204
|
}
|
|
@@ -313,9 +313,10 @@ var companionConfigSchema = z2.object({
|
|
|
313
313
|
// The change this task exists for: Claude Code used to be the one harness a
|
|
314
314
|
// device exposed with no configuration — a `claude --version` exit-0 was taken as
|
|
315
315
|
// consent — and it is now the third config-driven harness, opted into exactly
|
|
316
|
-
// like codex.
|
|
317
|
-
//
|
|
318
|
-
//
|
|
316
|
+
// like codex. CT1379: the binary that must be there is the Agent SDK's own
|
|
317
|
+
// bundled executable — the file a turn actually launches — not the user's
|
|
318
|
+
// `claude` on PATH, which the SDK never runs. The manifest advertises
|
|
319
|
+
// claude-code only when this block says so AND that binary resolves.
|
|
319
320
|
//
|
|
320
321
|
// The block's PRESENCE is also the migration marker (see `migrateConnectedHarnesses`).
|
|
321
322
|
// Absent means the config predates this task — a device whose user was never
|
|
@@ -335,9 +336,11 @@ var companionConfigSchema = z2.object({
|
|
|
335
336
|
// not a server address. The operator installs Codex + logs in (`codex login`, or
|
|
336
337
|
// `CODEX_API_KEY` — Cabane never sees the key) and sets `{ "codex": { "enabled":
|
|
337
338
|
// true } }`. Presence of the block enables it (an explicit `enabled: false`
|
|
338
|
-
// keeps the block but turns it off).
|
|
339
|
-
//
|
|
340
|
-
//
|
|
339
|
+
// keeps the block but turns it off). Absent → the device doesn't offer codex.
|
|
340
|
+
// CT1379: enabling is now half the answer, not all of it — the manifest and the
|
|
341
|
+
// adapter registry additionally require the SDK's vendored `codex` binary to
|
|
342
|
+
// resolve, because the enable flag alone advertised a runtime a device with a
|
|
343
|
+
// half-installed platform package could not start.
|
|
341
344
|
codex: z2.object({
|
|
342
345
|
enabled: z2.boolean().optional()
|
|
343
346
|
}).strict().optional()
|
|
@@ -426,15 +429,15 @@ function requireConfig() {
|
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
// src/dashboard/server.ts
|
|
429
|
-
import { dirname as
|
|
430
|
-
import { fileURLToPath } from "url";
|
|
432
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
433
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
431
434
|
import { serve } from "@hono/node-server";
|
|
432
435
|
import { Hono } from "hono";
|
|
433
436
|
|
|
434
437
|
// src/dashboard/routes.ts
|
|
435
438
|
import { openSync, readSync, closeSync, fstatSync, existsSync as existsSync2 } from "fs";
|
|
436
439
|
import { readFile } from "fs/promises";
|
|
437
|
-
import { extname, join as
|
|
440
|
+
import { extname, join as join4, normalize } from "path";
|
|
438
441
|
import { streamSSE } from "hono/streaming";
|
|
439
442
|
|
|
440
443
|
// src/harness-check.ts
|
|
@@ -450,14 +453,14 @@ function parseVersionToken(raw) {
|
|
|
450
453
|
return m ? m[0] : null;
|
|
451
454
|
}
|
|
452
455
|
function probeCliPresence(command, spawnImpl = spawn2) {
|
|
453
|
-
return new Promise((
|
|
456
|
+
return new Promise((resolve2) => {
|
|
454
457
|
let settled = false;
|
|
455
458
|
let timer = null;
|
|
456
459
|
const done = (result) => {
|
|
457
460
|
if (!settled) {
|
|
458
461
|
settled = true;
|
|
459
462
|
if (timer) clearTimeout(timer);
|
|
460
|
-
|
|
463
|
+
resolve2(result);
|
|
461
464
|
}
|
|
462
465
|
};
|
|
463
466
|
let child;
|
|
@@ -539,6 +542,203 @@ async function safe(fn) {
|
|
|
539
542
|
}
|
|
540
543
|
}
|
|
541
544
|
|
|
545
|
+
// src/bundled-binaries.ts
|
|
546
|
+
import { realpathSync, statSync } from "fs";
|
|
547
|
+
import { createRequire } from "module";
|
|
548
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
549
|
+
import { fileURLToPath } from "url";
|
|
550
|
+
function nodeResolveSelf(specifier) {
|
|
551
|
+
const here = fileURLToPath(import.meta.url);
|
|
552
|
+
if (typeof import.meta.resolve === "function") {
|
|
553
|
+
try {
|
|
554
|
+
return fileURLToPath(import.meta.resolve(specifier));
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
return createRequire(here).resolve(specifier);
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
return walkNodeModules(specifier, here);
|
|
563
|
+
}
|
|
564
|
+
function walkNodeModules(pkg2, fromPath) {
|
|
565
|
+
let dir2 = dirname2(fromPath);
|
|
566
|
+
for (; ; ) {
|
|
567
|
+
const manifest = join2(dir2, "node_modules", pkg2, "package.json");
|
|
568
|
+
if (nodeIsFile(manifest)) {
|
|
569
|
+
try {
|
|
570
|
+
return realpathSync(manifest);
|
|
571
|
+
} catch {
|
|
572
|
+
return manifest;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const parent = dirname2(dir2);
|
|
576
|
+
if (parent === dir2) return null;
|
|
577
|
+
dir2 = parent;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function nodeResolve(specifier, fromPath) {
|
|
581
|
+
try {
|
|
582
|
+
return createRequire(fromPath).resolve(specifier);
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function nodeIsFile(path) {
|
|
588
|
+
try {
|
|
589
|
+
return statSync(path).isFile();
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function detectPreferMusl(platform = process.platform) {
|
|
595
|
+
if (platform !== "linux") return false;
|
|
596
|
+
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
|
|
597
|
+
const header = report?.header;
|
|
598
|
+
return report != null && header?.glibcVersionRuntime === void 0;
|
|
599
|
+
}
|
|
600
|
+
function defaultResolveEnv() {
|
|
601
|
+
return {
|
|
602
|
+
platform: process.platform,
|
|
603
|
+
arch: process.arch,
|
|
604
|
+
preferMusl: detectPreferMusl(),
|
|
605
|
+
// Both resolutions start from this module, which tsup bundles into
|
|
606
|
+
// `dist/cli.js` and `dist/postinstall.js` — both at the package's `dist/`
|
|
607
|
+
// root — so one environment serves the runtime checks and the install
|
|
608
|
+
// verifier alike.
|
|
609
|
+
resolveSelf: nodeResolveSelf,
|
|
610
|
+
resolve: nodeResolve,
|
|
611
|
+
isFile: nodeIsFile
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
var CLAUDE_SDK = "@anthropic-ai/claude-agent-sdk";
|
|
615
|
+
function claudePlatformPackages(platform, arch, preferMusl) {
|
|
616
|
+
if (platform === "android") return [`${CLAUDE_SDK}-linux-${arch}-android`];
|
|
617
|
+
if (platform === "linux") {
|
|
618
|
+
return preferMusl ? [`${CLAUDE_SDK}-linux-${arch}-musl`, `${CLAUDE_SDK}-linux-${arch}`] : [`${CLAUDE_SDK}-linux-${arch}`, `${CLAUDE_SDK}-linux-${arch}-musl`];
|
|
619
|
+
}
|
|
620
|
+
return [`${CLAUDE_SDK}-${platform}-${arch}`];
|
|
621
|
+
}
|
|
622
|
+
function resolveClaude(env) {
|
|
623
|
+
const candidates = claudePlatformPackages(env.platform, env.arch, env.preferMusl);
|
|
624
|
+
const base = {
|
|
625
|
+
runtime: "claude-code",
|
|
626
|
+
packageName: candidates[0] ?? null,
|
|
627
|
+
platform: env.platform,
|
|
628
|
+
arch: env.arch
|
|
629
|
+
};
|
|
630
|
+
const sdkEntry = env.resolveSelf(CLAUDE_SDK);
|
|
631
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
632
|
+
let sawPackage = null;
|
|
633
|
+
for (const pkg2 of candidates) {
|
|
634
|
+
const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
|
|
635
|
+
if (!manifest) continue;
|
|
636
|
+
sawPackage ??= pkg2;
|
|
637
|
+
const exe = join2(dirname2(manifest), env.platform === "win32" ? "claude.exe" : "claude");
|
|
638
|
+
if (env.isFile(exe)) {
|
|
639
|
+
return { ...base, packageName: pkg2, status: "present", path: exe };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return sawPackage ? { ...base, packageName: sawPackage, status: "executable_missing", path: null } : { ...base, status: "platform_package_missing", path: null };
|
|
643
|
+
}
|
|
644
|
+
var CODEX_SDK = "@openai/codex-sdk";
|
|
645
|
+
var CODEX_CLI = "@openai/codex";
|
|
646
|
+
function codexTargetTriple(platform, arch) {
|
|
647
|
+
if (platform === "linux" || platform === "android") {
|
|
648
|
+
if (arch === "x64") return "x86_64-unknown-linux-musl";
|
|
649
|
+
if (arch === "arm64") return "aarch64-unknown-linux-musl";
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
if (platform === "darwin") {
|
|
653
|
+
if (arch === "x64") return "x86_64-apple-darwin";
|
|
654
|
+
if (arch === "arm64") return "aarch64-apple-darwin";
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
if (platform === "win32") {
|
|
658
|
+
if (arch === "x64") return "x86_64-pc-windows-msvc";
|
|
659
|
+
if (arch === "arm64") return "aarch64-pc-windows-msvc";
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
665
|
+
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
666
|
+
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
667
|
+
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
668
|
+
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
669
|
+
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
670
|
+
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
671
|
+
};
|
|
672
|
+
function resolveCodex(env) {
|
|
673
|
+
const triple = codexTargetTriple(env.platform, env.arch);
|
|
674
|
+
const pkg2 = triple ? CODEX_PACKAGE_BY_TRIPLE[triple] : void 0;
|
|
675
|
+
const base = {
|
|
676
|
+
runtime: "codex",
|
|
677
|
+
packageName: pkg2 ?? null,
|
|
678
|
+
platform: env.platform,
|
|
679
|
+
arch: env.arch
|
|
680
|
+
};
|
|
681
|
+
if (!triple || !pkg2) return { ...base, status: "platform_package_missing", path: null };
|
|
682
|
+
const sdkEntry = env.resolveSelf(CODEX_SDK);
|
|
683
|
+
if (!sdkEntry) return { ...base, status: "platform_package_missing", path: null };
|
|
684
|
+
const cliManifest = env.resolve(`${CODEX_CLI}/package.json`, sdkEntry);
|
|
685
|
+
if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
686
|
+
const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
|
|
687
|
+
if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
|
|
688
|
+
const packageRoot = join2(dirname2(platformManifest), "vendor", triple);
|
|
689
|
+
const exeName = env.platform === "win32" ? "codex.exe" : "codex";
|
|
690
|
+
const current = join2(packageRoot, "bin", exeName);
|
|
691
|
+
if (env.isFile(current) && env.isFile(join2(packageRoot, "codex-package.json"))) {
|
|
692
|
+
return { ...base, status: "present", path: current };
|
|
693
|
+
}
|
|
694
|
+
const legacy = join2(packageRoot, "codex", exeName);
|
|
695
|
+
if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
|
|
696
|
+
return { ...base, status: "executable_missing", path: null };
|
|
697
|
+
}
|
|
698
|
+
function resolveBundledBinary(runtime, env = defaultResolveEnv()) {
|
|
699
|
+
try {
|
|
700
|
+
return runtime === "codex" ? resolveCodex(env) : resolveClaude(env);
|
|
701
|
+
} catch {
|
|
702
|
+
return {
|
|
703
|
+
runtime,
|
|
704
|
+
status: "platform_package_missing",
|
|
705
|
+
path: null,
|
|
706
|
+
packageName: null,
|
|
707
|
+
platform: env.platform,
|
|
708
|
+
arch: env.arch
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
var HARNESS_LABEL = {
|
|
713
|
+
"claude-code": "Claude Code",
|
|
714
|
+
codex: "Codex"
|
|
715
|
+
};
|
|
716
|
+
var HARNESS_INTENT_WORD = {
|
|
717
|
+
"claude-code": "connected",
|
|
718
|
+
codex: "enabled"
|
|
719
|
+
};
|
|
720
|
+
function missingNoun(status) {
|
|
721
|
+
return status === "executable_missing" ? "executable" : "platform package";
|
|
722
|
+
}
|
|
723
|
+
function repairCommands() {
|
|
724
|
+
return ["npm i -g @cabane/companion --include=optional"];
|
|
725
|
+
}
|
|
726
|
+
function repairAction() {
|
|
727
|
+
return `Reinstall with \`${repairCommands()[0]}\`.`;
|
|
728
|
+
}
|
|
729
|
+
function startupWarning(r) {
|
|
730
|
+
const label = HARNESS_LABEL[r.runtime];
|
|
731
|
+
return `warning: ${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but @cabane/companion's bundled ${label} ${missingNoun(r.status)} is missing. It will not be offered to Cabane. ${repairAction()} Then restart the companion.`;
|
|
732
|
+
}
|
|
733
|
+
function harnessIssueNote(runtime, status) {
|
|
734
|
+
const intent = HARNESS_INTENT_WORD[runtime];
|
|
735
|
+
const capitalized = `${intent[0].toUpperCase()}${intent.slice(1)}`;
|
|
736
|
+
return `${capitalized}, but @cabane/companion's bundled ${HARNESS_LABEL[runtime]} ${missingNoun(status)} is missing. ${repairAction()} Then restart the companion.`;
|
|
737
|
+
}
|
|
738
|
+
function turnIncompleteCopy(runtime) {
|
|
739
|
+
return `**This device's ${HARNESS_LABEL[runtime]} runtime is incomplete.** ${repairAction()} Restart it, then try again.`;
|
|
740
|
+
}
|
|
741
|
+
|
|
542
742
|
// src/manifest.ts
|
|
543
743
|
var DEVICE_MANIFEST = {
|
|
544
744
|
runtimes: [{ name: "claude-code", version: null }],
|
|
@@ -550,7 +750,20 @@ function buildCompanionManifest(opts) {
|
|
|
550
750
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
551
751
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
552
752
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
553
|
-
return {
|
|
753
|
+
return {
|
|
754
|
+
runtimes,
|
|
755
|
+
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
756
|
+
runtimeIssues: opts.runtimeIssues ?? []
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function runtimeIssuesFor(gates) {
|
|
760
|
+
const issues = [];
|
|
761
|
+
for (const name of ["claude-code", "codex"]) {
|
|
762
|
+
const gate = name === "codex" ? gates.codex : gates.claudeCode;
|
|
763
|
+
if (!gate.intended || gate.bundled === "present") continue;
|
|
764
|
+
issues.push({ name, reason: gate.bundled });
|
|
765
|
+
}
|
|
766
|
+
return issues;
|
|
554
767
|
}
|
|
555
768
|
|
|
556
769
|
// src/harness-status.ts
|
|
@@ -564,11 +777,12 @@ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
|
|
|
564
777
|
function deriveHarnessSnapshot(signals) {
|
|
565
778
|
const advertised = new Set(
|
|
566
779
|
buildCompanionManifest({
|
|
567
|
-
// CT1082: connected AND
|
|
568
|
-
// through the same function
|
|
569
|
-
|
|
780
|
+
// CT1082 + CT1379: connected AND the bundled binary the SDK launches is
|
|
781
|
+
// there — the manifest's own rule, restated here through the same function
|
|
782
|
+
// rather than re-decided. PATH is deliberately absent from both gates now.
|
|
783
|
+
claudeCode: signals.claudeCodeConnected && signals.claudeBundled === "present",
|
|
570
784
|
opencode: signals.opencodeConfigured,
|
|
571
|
-
codex: signals.codexEnabled
|
|
785
|
+
codex: signals.codexEnabled && signals.codexBundled === "present"
|
|
572
786
|
}).runtimes.map((r) => r.name)
|
|
573
787
|
);
|
|
574
788
|
const harnesses = [
|
|
@@ -594,7 +808,7 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
594
808
|
...base,
|
|
595
809
|
state: "needs_attention",
|
|
596
810
|
version: null,
|
|
597
|
-
detail: "
|
|
811
|
+
detail: harnessIssueNote("claude-code", signals.claudeBundled),
|
|
598
812
|
enable: null
|
|
599
813
|
};
|
|
600
814
|
}
|
|
@@ -603,7 +817,11 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
603
817
|
...base,
|
|
604
818
|
state: "detected_not_exposed",
|
|
605
819
|
version: signals.claudeVersion,
|
|
606
|
-
|
|
820
|
+
// CT1379: connecting records INTENT; it is not the same as becoming
|
|
821
|
+
// runnable. With the bundled binary missing, a connect succeeds and the
|
|
822
|
+
// device still advertises nothing — so this says so up front rather than
|
|
823
|
+
// promising routing it can't deliver.
|
|
824
|
+
detail: signals.claudeBundled === "present" ? "Claude Code is installed here but not connected yet. Connect it to offer Claude Code from this device." : `Claude Code is installed here but not connected yet \u2014 and @cabane/companion's bundled Claude Code ${missingNoun(signals.claudeBundled)} is missing, so connecting alone won't make it runnable. ${repairAction()}`,
|
|
607
825
|
enable: "claude-code"
|
|
608
826
|
};
|
|
609
827
|
}
|
|
@@ -618,20 +836,20 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
618
836
|
function deriveCodex(signals, manifestHas) {
|
|
619
837
|
const base = { runtime: "codex", label: LABELS.codex };
|
|
620
838
|
if (manifestHas) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
839
|
+
return {
|
|
840
|
+
...base,
|
|
841
|
+
state: "exposed",
|
|
842
|
+
version: signals.codexVersion,
|
|
843
|
+
detail: "Codex is enabled and exposed to Cabane.",
|
|
844
|
+
enable: null
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (signals.codexEnabled) {
|
|
630
848
|
return {
|
|
631
849
|
...base,
|
|
632
850
|
state: "needs_attention",
|
|
633
851
|
version: null,
|
|
634
|
-
detail: "
|
|
852
|
+
detail: harnessIssueNote("codex", signals.codexBundled),
|
|
635
853
|
enable: null
|
|
636
854
|
};
|
|
637
855
|
}
|
|
@@ -640,7 +858,9 @@ function deriveCodex(signals, manifestHas) {
|
|
|
640
858
|
...base,
|
|
641
859
|
state: "detected_not_exposed",
|
|
642
860
|
version: signals.codexVersion,
|
|
643
|
-
|
|
861
|
+
// CT1379: same distinction as Claude Code — enabling is intent, not
|
|
862
|
+
// launchability.
|
|
863
|
+
detail: signals.codexBundled === "present" ? "Codex is installed but not enabled yet. Turn it on to offer Codex from this device." : `Codex is installed but not enabled yet \u2014 and @cabane/companion's bundled Codex ${missingNoun(signals.codexBundled)} is missing, so enabling alone won't make it runnable. ${repairAction()}`,
|
|
644
864
|
enable: "codex"
|
|
645
865
|
};
|
|
646
866
|
}
|
|
@@ -707,6 +927,7 @@ async function resolveOpencodeServerUrl(configuredServerUrl, requestedServerUrl,
|
|
|
707
927
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
708
928
|
const probePresence = deps.probePresence ?? probeHarnessPresence;
|
|
709
929
|
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
930
|
+
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
710
931
|
const configuredServerUrl = cfg.opencode?.serverUrl;
|
|
711
932
|
const opencodeProbeUrl = configuredServerUrl ?? DEFAULT_OPENCODE_SERVER_URL;
|
|
712
933
|
const [claudePresence, codexPresence, opencodeVersion] = await Promise.all([
|
|
@@ -722,6 +943,11 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
722
943
|
// CT1082: the user's opt-in. Presence alone exposes nothing now, so this is
|
|
723
944
|
// the manifest gate and the probe above is only a suggestion.
|
|
724
945
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
946
|
+
// CT1379: the executable a Claude turn actually launches. Resolved every time
|
|
947
|
+
// the signals are, so a binary that disappears between beats is caught on the
|
|
948
|
+
// next one rather than at turn time.
|
|
949
|
+
claudeBundled: resolveBundled("claude-code").status,
|
|
950
|
+
codexBundled: resolveBundled("codex").status,
|
|
725
951
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
726
952
|
// exposes codex; its config flag is the manifest gate either way).
|
|
727
953
|
codexOnPath: codexVersion !== null,
|
|
@@ -735,12 +961,12 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
735
961
|
};
|
|
736
962
|
}
|
|
737
963
|
function withTimeout(promise, fallback, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
738
|
-
return new Promise((
|
|
964
|
+
return new Promise((resolve2) => {
|
|
739
965
|
let settled = false;
|
|
740
966
|
const done = (v) => {
|
|
741
967
|
if (!settled) {
|
|
742
968
|
settled = true;
|
|
743
|
-
|
|
969
|
+
resolve2(v);
|
|
744
970
|
}
|
|
745
971
|
};
|
|
746
972
|
const timer = setTimeout(() => done(fallback), timeoutMs);
|
|
@@ -818,19 +1044,19 @@ function looksUnsupported(output) {
|
|
|
818
1044
|
);
|
|
819
1045
|
}
|
|
820
1046
|
function runBounded(command, args) {
|
|
821
|
-
return new Promise((
|
|
1047
|
+
return new Promise((resolve2) => {
|
|
822
1048
|
let settled = false;
|
|
823
1049
|
const done = (result) => {
|
|
824
1050
|
if (settled) return;
|
|
825
1051
|
settled = true;
|
|
826
1052
|
clearTimeout(timer);
|
|
827
|
-
|
|
1053
|
+
resolve2(result);
|
|
828
1054
|
};
|
|
829
1055
|
let child;
|
|
830
1056
|
try {
|
|
831
1057
|
child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
832
1058
|
} catch {
|
|
833
|
-
|
|
1059
|
+
resolve2({ code: null, output: "", error: "spawn" });
|
|
834
1060
|
return;
|
|
835
1061
|
}
|
|
836
1062
|
let out = "";
|
|
@@ -851,11 +1077,11 @@ function runBounded(command, args) {
|
|
|
851
1077
|
|
|
852
1078
|
// src/logger.ts
|
|
853
1079
|
import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
|
|
854
|
-
import { dirname as
|
|
1080
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
855
1081
|
import pino from "pino";
|
|
856
1082
|
import pretty from "pino-pretty";
|
|
857
1083
|
function companionLogPath() {
|
|
858
|
-
return
|
|
1084
|
+
return join3(cabaneDir(), "companion.log");
|
|
859
1085
|
}
|
|
860
1086
|
var CONSOLE_IGNORE = [
|
|
861
1087
|
"pid",
|
|
@@ -880,7 +1106,7 @@ var cached = null;
|
|
|
880
1106
|
var consoleLogging = true;
|
|
881
1107
|
function createLogger(destinations = {}) {
|
|
882
1108
|
const path = companionLogPath();
|
|
883
|
-
if (!destinations.file) mkdirSync2(
|
|
1109
|
+
if (!destinations.file) mkdirSync2(dirname3(path), { recursive: true });
|
|
884
1110
|
const streams = [];
|
|
885
1111
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
886
1112
|
const consoleStream = pretty({
|
|
@@ -1156,13 +1382,13 @@ var CONTENT_TYPES = {
|
|
|
1156
1382
|
function registerRoutes(app, deps) {
|
|
1157
1383
|
const { supervisor, hub, staticDir } = deps;
|
|
1158
1384
|
app.get("/", async (c) => {
|
|
1159
|
-
const html = await readFile(
|
|
1385
|
+
const html = await readFile(join4(staticDir, "index.html"), "utf8");
|
|
1160
1386
|
return c.html(html);
|
|
1161
1387
|
});
|
|
1162
1388
|
app.get("/static/:file", async (c) => {
|
|
1163
1389
|
const file = c.req.param("file");
|
|
1164
1390
|
const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
1165
|
-
const full =
|
|
1391
|
+
const full = join4(staticDir, safe3);
|
|
1166
1392
|
if (!full.startsWith(staticDir) || !existsSync2(full)) return c.notFound();
|
|
1167
1393
|
const body = await readFile(full);
|
|
1168
1394
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
@@ -1262,8 +1488,8 @@ function registerRoutes(app, deps) {
|
|
|
1262
1488
|
while (!stream.aborted) {
|
|
1263
1489
|
if (queue.length === 0) {
|
|
1264
1490
|
await Promise.race([
|
|
1265
|
-
new Promise((
|
|
1266
|
-
wake =
|
|
1491
|
+
new Promise((resolve2) => {
|
|
1492
|
+
wake = resolve2;
|
|
1267
1493
|
}),
|
|
1268
1494
|
stream.sleep(25e3)
|
|
1269
1495
|
]);
|
|
@@ -1347,8 +1573,8 @@ async function startDashboard(opts) {
|
|
|
1347
1573
|
return {
|
|
1348
1574
|
url,
|
|
1349
1575
|
port,
|
|
1350
|
-
close: () => new Promise((
|
|
1351
|
-
server.close(() =>
|
|
1576
|
+
close: () => new Promise((resolve2) => {
|
|
1577
|
+
server.close(() => resolve2());
|
|
1352
1578
|
server.closeAllConnections?.();
|
|
1353
1579
|
})
|
|
1354
1580
|
};
|
|
@@ -1365,12 +1591,12 @@ async function startDashboard(opts) {
|
|
|
1365
1591
|
);
|
|
1366
1592
|
}
|
|
1367
1593
|
function listen(app, port) {
|
|
1368
|
-
return new Promise((
|
|
1594
|
+
return new Promise((resolve2, reject) => {
|
|
1369
1595
|
let settled = false;
|
|
1370
1596
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => {
|
|
1371
1597
|
if (!settled) {
|
|
1372
1598
|
settled = true;
|
|
1373
|
-
|
|
1599
|
+
resolve2(server);
|
|
1374
1600
|
}
|
|
1375
1601
|
});
|
|
1376
1602
|
server.on("error", (err) => {
|
|
@@ -1385,14 +1611,14 @@ function isAddrInUse(err) {
|
|
|
1385
1611
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1386
1612
|
}
|
|
1387
1613
|
function resolveStaticDir() {
|
|
1388
|
-
return
|
|
1614
|
+
return join5(dirname4(fileURLToPath2(import.meta.url)), "static");
|
|
1389
1615
|
}
|
|
1390
1616
|
|
|
1391
1617
|
// src/control-socket.ts
|
|
1392
1618
|
import { createHash } from "crypto";
|
|
1393
1619
|
import { chmodSync as chmodSync2, existsSync as existsSync3, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
1394
1620
|
import { createServer, connect } from "net";
|
|
1395
|
-
import { join as
|
|
1621
|
+
import { join as join6 } from "path";
|
|
1396
1622
|
var CONTROL_TIMEOUT_MS = 1e3;
|
|
1397
1623
|
var MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
1398
1624
|
function controlSocketPath() {
|
|
@@ -1401,11 +1627,11 @@ function controlSocketPath() {
|
|
|
1401
1627
|
if (process.platform === "win32") {
|
|
1402
1628
|
return `\\\\.\\pipe\\cabane-companion-${key}`;
|
|
1403
1629
|
}
|
|
1404
|
-
const besideRuntime =
|
|
1630
|
+
const besideRuntime = join6(dir2, "companion.sock");
|
|
1405
1631
|
if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
|
|
1406
1632
|
const filename = `cabane-companion-${key}.sock`;
|
|
1407
|
-
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ?
|
|
1408
|
-
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath :
|
|
1633
|
+
const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync3(process.env.XDG_RUNTIME_DIR) ? join6(process.env.XDG_RUNTIME_DIR, filename) : null;
|
|
1634
|
+
return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join6("/tmp", filename);
|
|
1409
1635
|
}
|
|
1410
1636
|
async function startControlServer(handlers) {
|
|
1411
1637
|
const path = controlSocketPath();
|
|
@@ -1419,18 +1645,18 @@ async function startControlServer(handlers) {
|
|
|
1419
1645
|
void serveConnection(socket, handlers);
|
|
1420
1646
|
});
|
|
1421
1647
|
server.unref();
|
|
1422
|
-
await new Promise((
|
|
1648
|
+
await new Promise((resolve2, reject) => {
|
|
1423
1649
|
server.once("error", reject);
|
|
1424
1650
|
server.listen(path, () => {
|
|
1425
1651
|
server.removeListener("error", reject);
|
|
1426
|
-
|
|
1652
|
+
resolve2();
|
|
1427
1653
|
});
|
|
1428
1654
|
});
|
|
1429
1655
|
if (process.platform !== "win32") {
|
|
1430
1656
|
try {
|
|
1431
1657
|
chmodSync2(path, 384);
|
|
1432
1658
|
} catch (err) {
|
|
1433
|
-
await new Promise((
|
|
1659
|
+
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
1434
1660
|
try {
|
|
1435
1661
|
rmSync2(path, { force: true });
|
|
1436
1662
|
} catch {
|
|
@@ -1442,10 +1668,10 @@ async function startControlServer(handlers) {
|
|
|
1442
1668
|
});
|
|
1443
1669
|
return {
|
|
1444
1670
|
path,
|
|
1445
|
-
close: () => new Promise((
|
|
1671
|
+
close: () => new Promise((resolve2) => {
|
|
1446
1672
|
server.close(() => {
|
|
1447
1673
|
if (process.platform !== "win32") rmSync2(path, { force: true });
|
|
1448
|
-
|
|
1674
|
+
resolve2();
|
|
1449
1675
|
});
|
|
1450
1676
|
})
|
|
1451
1677
|
};
|
|
@@ -1495,12 +1721,12 @@ function reply(socket, body) {
|
|
|
1495
1721
|
async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
|
|
1496
1722
|
const socket = connect(path);
|
|
1497
1723
|
try {
|
|
1498
|
-
await new Promise((
|
|
1724
|
+
await new Promise((resolve2, reject) => {
|
|
1499
1725
|
const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
|
|
1500
1726
|
timer.unref?.();
|
|
1501
1727
|
socket.once("connect", () => {
|
|
1502
1728
|
clearTimeout(timer);
|
|
1503
|
-
|
|
1729
|
+
resolve2();
|
|
1504
1730
|
});
|
|
1505
1731
|
socket.once("error", (err) => {
|
|
1506
1732
|
clearTimeout(timer);
|
|
@@ -1527,7 +1753,7 @@ function isNotListening(err) {
|
|
|
1527
1753
|
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
1528
1754
|
}
|
|
1529
1755
|
function readLine(socket, timeoutMs) {
|
|
1530
|
-
return new Promise((
|
|
1756
|
+
return new Promise((resolve2) => {
|
|
1531
1757
|
let buf = "";
|
|
1532
1758
|
let settled = false;
|
|
1533
1759
|
const done = (v) => {
|
|
@@ -1535,7 +1761,7 @@ function readLine(socket, timeoutMs) {
|
|
|
1535
1761
|
settled = true;
|
|
1536
1762
|
clearTimeout(timer);
|
|
1537
1763
|
socket.removeListener("data", onData);
|
|
1538
|
-
|
|
1764
|
+
resolve2(v);
|
|
1539
1765
|
};
|
|
1540
1766
|
const timer = setTimeout(() => done(null), timeoutMs);
|
|
1541
1767
|
timer.unref?.();
|
|
@@ -1581,6 +1807,7 @@ async function requireStartConfig(deps = {}) {
|
|
|
1581
1807
|
async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
1582
1808
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
1583
1809
|
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
1810
|
+
const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
1584
1811
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
1585
1812
|
`));
|
|
1586
1813
|
const connected = [
|
|
@@ -1589,25 +1816,42 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1589
1816
|
...cfg.opencode ? ["opencode"] : []
|
|
1590
1817
|
];
|
|
1591
1818
|
if (connected.length > 0) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
);
|
|
1819
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
1820
|
+
const intended = runtime === "codex" ? isCodexEnabled(cfg) : isClaudeCodeConnected(cfg);
|
|
1821
|
+
if (!intended) continue;
|
|
1822
|
+
const resolution = resolveBundled(runtime);
|
|
1823
|
+
if (resolution.status !== "present") warn(startupWarning(resolution));
|
|
1596
1824
|
}
|
|
1597
1825
|
return;
|
|
1598
1826
|
}
|
|
1599
1827
|
const [claudeInstalled, codexInstalled] = await Promise.all([probeClaude(), probeCodex()]);
|
|
1600
|
-
const
|
|
1601
|
-
...claudeInstalled ? [
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1828
|
+
const found = [
|
|
1829
|
+
...claudeInstalled ? [
|
|
1830
|
+
{
|
|
1831
|
+
label: "Claude Code",
|
|
1832
|
+
command: "`cabane-companion connect claude-code`",
|
|
1833
|
+
bundled: resolveBundled("claude-code").status
|
|
1834
|
+
}
|
|
1835
|
+
] : [],
|
|
1836
|
+
...codexInstalled ? [
|
|
1837
|
+
{
|
|
1838
|
+
label: "Codex",
|
|
1839
|
+
command: "`cabane-companion connect codex`",
|
|
1840
|
+
bundled: resolveBundled("codex").status
|
|
1841
|
+
}
|
|
1842
|
+
] : []
|
|
1607
1843
|
];
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1844
|
+
const runnable = found.filter((h) => h.bundled === "present");
|
|
1845
|
+
const incomplete = found.filter((h) => h.bundled !== "present");
|
|
1846
|
+
const incompleteNote = incomplete.map(
|
|
1847
|
+
(h) => `${h.label} is installed here, but @cabane/companion's bundled ${h.label} ${missingNoun(h.bundled)} is missing, so connecting it alone won't make it runnable.`
|
|
1848
|
+
).join(" ");
|
|
1849
|
+
const guidance = runnable.length > 0 ? `We found ${runnable.map((h) => h.label).join(" and ")} on this machine \u2014 connect ${runnable.length > 1 ? "one with " : "it with "}${runnable.map((h) => h.command).join(" or ")} and turns start routing here.${incompleteNote ? ` ${incompleteNote} ${repairAction()}` : ""}` : incomplete.length > 0 ? (
|
|
1850
|
+
// Their machine is fine; this companion's install is not. Repair that,
|
|
1851
|
+
// then connect — no "install a harness" they already have.
|
|
1852
|
+
`${incompleteNote} ${repairAction()} Then connect it with ${incomplete.map((h) => h.command).join(" or ")}.`
|
|
1853
|
+
) : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).";
|
|
1854
|
+
warn(`No harness is connected on this device yet, so no agent turn can run here. ${guidance}`);
|
|
1611
1855
|
}
|
|
1612
1856
|
|
|
1613
1857
|
// src/runtime-file.ts
|
|
@@ -1620,10 +1864,10 @@ import {
|
|
|
1620
1864
|
openSync as openSync2,
|
|
1621
1865
|
closeSync as closeSync2
|
|
1622
1866
|
} from "fs";
|
|
1623
|
-
import { join as
|
|
1867
|
+
import { join as join7 } from "path";
|
|
1624
1868
|
var PROBE_TIMEOUT_MS2 = 1e3;
|
|
1625
1869
|
function runtimePath() {
|
|
1626
|
-
return
|
|
1870
|
+
return join7(cabaneDir(), "runtime.json");
|
|
1627
1871
|
}
|
|
1628
1872
|
function serialize(state) {
|
|
1629
1873
|
return JSON.stringify(state, null, 2) + "\n";
|
|
@@ -2173,15 +2417,15 @@ function isAbortError(err) {
|
|
|
2173
2417
|
return err instanceof Error && err.name === "AbortError";
|
|
2174
2418
|
}
|
|
2175
2419
|
function sleep(ms, signal) {
|
|
2176
|
-
return new Promise((
|
|
2177
|
-
if (signal?.aborted) return
|
|
2420
|
+
return new Promise((resolve2) => {
|
|
2421
|
+
if (signal?.aborted) return resolve2();
|
|
2178
2422
|
const timer = setTimeout(() => {
|
|
2179
2423
|
signal?.removeEventListener("abort", onAbort);
|
|
2180
|
-
|
|
2424
|
+
resolve2();
|
|
2181
2425
|
}, ms);
|
|
2182
2426
|
const onAbort = () => {
|
|
2183
2427
|
clearTimeout(timer);
|
|
2184
|
-
|
|
2428
|
+
resolve2();
|
|
2185
2429
|
};
|
|
2186
2430
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2187
2431
|
});
|
|
@@ -2272,10 +2516,10 @@ import {
|
|
|
2272
2516
|
rmSync as rmSync4,
|
|
2273
2517
|
writeFileSync as writeFileSync3
|
|
2274
2518
|
} from "fs";
|
|
2275
|
-
import { dirname as
|
|
2519
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
2276
2520
|
import { z as z3 } from "zod";
|
|
2277
2521
|
function credentialsPath() {
|
|
2278
|
-
return
|
|
2522
|
+
return join8(cabaneDir(), "credentials.json");
|
|
2279
2523
|
}
|
|
2280
2524
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
2281
2525
|
function load() {
|
|
@@ -2297,7 +2541,7 @@ function load() {
|
|
|
2297
2541
|
}
|
|
2298
2542
|
function save(map) {
|
|
2299
2543
|
const path = credentialsPath();
|
|
2300
|
-
mkdirSync5(
|
|
2544
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
2301
2545
|
try {
|
|
2302
2546
|
chmodSync3(cabaneDir(), 448);
|
|
2303
2547
|
} catch {
|
|
@@ -2343,9 +2587,9 @@ function pruneCredentials(keepAgentIds) {
|
|
|
2343
2587
|
|
|
2344
2588
|
// src/cursor.ts
|
|
2345
2589
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2346
|
-
import { join as
|
|
2590
|
+
import { join as join9 } from "path";
|
|
2347
2591
|
function pathFor(workspaceId) {
|
|
2348
|
-
return
|
|
2592
|
+
return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
2349
2593
|
}
|
|
2350
2594
|
function readCursor(workspaceId) {
|
|
2351
2595
|
const path = pathFor(workspaceId);
|
|
@@ -2355,7 +2599,7 @@ function readCursor(workspaceId) {
|
|
|
2355
2599
|
}
|
|
2356
2600
|
function writeCursor(workspaceId, eventId) {
|
|
2357
2601
|
const path = pathFor(workspaceId);
|
|
2358
|
-
mkdirSync6(
|
|
2602
|
+
mkdirSync6(join9(cabaneDir(), "cursors"), { recursive: true });
|
|
2359
2603
|
writeFileSync4(path, eventId + "\n", "utf8");
|
|
2360
2604
|
}
|
|
2361
2605
|
|
|
@@ -2400,13 +2644,13 @@ var CursorTracker = class {
|
|
|
2400
2644
|
|
|
2401
2645
|
// src/dispatch-dedupe.ts
|
|
2402
2646
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
|
|
2403
|
-
import { join as
|
|
2647
|
+
import { join as join10 } from "path";
|
|
2404
2648
|
var MAX_IDS = 256;
|
|
2405
2649
|
function dir(log) {
|
|
2406
|
-
return
|
|
2650
|
+
return join10(cabaneDir(), log);
|
|
2407
2651
|
}
|
|
2408
2652
|
function pathFor2(log, workspaceId) {
|
|
2409
|
-
return
|
|
2653
|
+
return join10(dir(log), encodeURIComponent(workspaceId));
|
|
2410
2654
|
}
|
|
2411
2655
|
function readIds(log, workspaceId) {
|
|
2412
2656
|
const path = pathFor2(log, workspaceId);
|
|
@@ -2443,10 +2687,10 @@ function markCompleted(workspaceId, eventId) {
|
|
|
2443
2687
|
}
|
|
2444
2688
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
2445
2689
|
function resumeDir() {
|
|
2446
|
-
return
|
|
2690
|
+
return join10(cabaneDir(), "resume-attempts");
|
|
2447
2691
|
}
|
|
2448
2692
|
function resumePathFor(workspaceId) {
|
|
2449
|
-
return
|
|
2693
|
+
return join10(resumeDir(), encodeURIComponent(workspaceId));
|
|
2450
2694
|
}
|
|
2451
2695
|
function readResumeCounts(workspaceId) {
|
|
2452
2696
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -2482,10 +2726,10 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
2482
2726
|
return next;
|
|
2483
2727
|
}
|
|
2484
2728
|
function turnDir() {
|
|
2485
|
-
return
|
|
2729
|
+
return join10(cabaneDir(), "turns");
|
|
2486
2730
|
}
|
|
2487
2731
|
function turnPathFor(workspaceId) {
|
|
2488
|
-
return
|
|
2732
|
+
return join10(turnDir(), encodeURIComponent(workspaceId));
|
|
2489
2733
|
}
|
|
2490
2734
|
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2491
2735
|
function readTurnIds(workspaceId) {
|
|
@@ -4860,7 +5104,7 @@ var serverTurnTails = /* @__PURE__ */ new Map();
|
|
|
4860
5104
|
async function acquireServerTurnLock(url) {
|
|
4861
5105
|
const prev = serverTurnTails.get(url) ?? Promise.resolve();
|
|
4862
5106
|
let release;
|
|
4863
|
-
const done = new Promise((
|
|
5107
|
+
const done = new Promise((resolve2) => release = resolve2);
|
|
4864
5108
|
serverTurnTails.set(url, done);
|
|
4865
5109
|
await prev.catch(() => {
|
|
4866
5110
|
});
|
|
@@ -7099,8 +7343,8 @@ var ConnectorHealthStore = class {
|
|
|
7099
7343
|
};
|
|
7100
7344
|
|
|
7101
7345
|
// src/dispatcher.ts
|
|
7102
|
-
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
7103
|
-
import { join as
|
|
7346
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7347
|
+
import { join as join15 } from "path";
|
|
7104
7348
|
|
|
7105
7349
|
// src/turn-execution.ts
|
|
7106
7350
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
@@ -7462,11 +7706,11 @@ function trimSlash2(s) {
|
|
|
7462
7706
|
// src/codex-instructions.ts
|
|
7463
7707
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7464
7708
|
import { tmpdir } from "os";
|
|
7465
|
-
import { join as
|
|
7709
|
+
import { join as join11 } from "path";
|
|
7466
7710
|
var PREFIX = "cabane-codex-instructions-";
|
|
7467
7711
|
async function writeCodexInstructionsFile(contents) {
|
|
7468
|
-
const dir2 = await mkdtemp(
|
|
7469
|
-
const path =
|
|
7712
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
7713
|
+
const path = join11(dir2, "instructions.md");
|
|
7470
7714
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7471
7715
|
return {
|
|
7472
7716
|
path,
|
|
@@ -7476,6 +7720,42 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7476
7720
|
};
|
|
7477
7721
|
}
|
|
7478
7722
|
|
|
7723
|
+
// src/api-error-shape.ts
|
|
7724
|
+
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
7725
|
+
"dispatch_not_admitted",
|
|
7726
|
+
"turn_already_ended",
|
|
7727
|
+
"turn_belongs_elsewhere"
|
|
7728
|
+
]);
|
|
7729
|
+
function apiErrorCode(err) {
|
|
7730
|
+
if (!(err instanceof ApiError)) return null;
|
|
7731
|
+
const body = err.body;
|
|
7732
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
7733
|
+
}
|
|
7734
|
+
function leaseRefusal(err) {
|
|
7735
|
+
const code = apiErrorCode(err);
|
|
7736
|
+
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
7737
|
+
return null;
|
|
7738
|
+
}
|
|
7739
|
+
function isWriteFenceRefusal(err) {
|
|
7740
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
7741
|
+
}
|
|
7742
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
7743
|
+
function describeErrorBody(body) {
|
|
7744
|
+
if (body === void 0 || body === null) return void 0;
|
|
7745
|
+
let text;
|
|
7746
|
+
if (typeof body === "string") {
|
|
7747
|
+
text = body;
|
|
7748
|
+
} else {
|
|
7749
|
+
try {
|
|
7750
|
+
text = JSON.stringify(body);
|
|
7751
|
+
} catch {
|
|
7752
|
+
text = String(body);
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
if (text.length === 0) return void 0;
|
|
7756
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7479
7759
|
// src/turn-seq-floor.ts
|
|
7480
7760
|
var SeqFloorUnavailable = class extends Error {
|
|
7481
7761
|
constructor(detail) {
|
|
@@ -7511,15 +7791,15 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
7511
7791
|
|
|
7512
7792
|
// src/prepared.ts
|
|
7513
7793
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
7514
|
-
import { join as
|
|
7794
|
+
import { join as join12 } from "path";
|
|
7515
7795
|
function dirFor(workspaceId) {
|
|
7516
|
-
return
|
|
7796
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
7517
7797
|
}
|
|
7518
7798
|
function conversationDir(workspaceId, conversationId) {
|
|
7519
|
-
return
|
|
7799
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7520
7800
|
}
|
|
7521
7801
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
7522
|
-
return
|
|
7802
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7523
7803
|
}
|
|
7524
7804
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
7525
7805
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
@@ -7551,11 +7831,11 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
7551
7831
|
|
|
7552
7832
|
// src/secrets.ts
|
|
7553
7833
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
7554
|
-
import { join as
|
|
7834
|
+
import { join as join13 } from "path";
|
|
7555
7835
|
import { z as z14 } from "zod";
|
|
7556
7836
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7557
7837
|
function secretsPath() {
|
|
7558
|
-
return
|
|
7838
|
+
return join13(cabaneDir(), "secrets.json");
|
|
7559
7839
|
}
|
|
7560
7840
|
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
7561
7841
|
function loadSecretStore() {
|
|
@@ -7642,9 +7922,9 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
7642
7922
|
|
|
7643
7923
|
// src/transcript-writer.ts
|
|
7644
7924
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
7645
|
-
import { basename, dirname as
|
|
7925
|
+
import { basename, dirname as dirname6, join as join14 } from "path";
|
|
7646
7926
|
function transcriptsDir() {
|
|
7647
|
-
return
|
|
7927
|
+
return join14(cabaneDir(), "transcripts");
|
|
7648
7928
|
}
|
|
7649
7929
|
var RETAIN = 200;
|
|
7650
7930
|
var ANOMALY_RETAIN = 50;
|
|
@@ -7654,7 +7934,7 @@ var TranscriptWriter = class {
|
|
|
7654
7934
|
onWarn;
|
|
7655
7935
|
constructor(dir2, meta, onWarn) {
|
|
7656
7936
|
this.onWarn = onWarn;
|
|
7657
|
-
this.path =
|
|
7937
|
+
this.path = join14(dir2, fileName(meta));
|
|
7658
7938
|
try {
|
|
7659
7939
|
mkdirSync9(dir2, { recursive: true });
|
|
7660
7940
|
try {
|
|
@@ -7683,10 +7963,10 @@ var TranscriptWriter = class {
|
|
|
7683
7963
|
preserveAnomaly() {
|
|
7684
7964
|
if (this.broken) return;
|
|
7685
7965
|
try {
|
|
7686
|
-
const dir2 =
|
|
7966
|
+
const dir2 = join14(dirname6(this.path), "anomalies");
|
|
7687
7967
|
mkdirSync9(dir2, { recursive: true, mode: 448 });
|
|
7688
7968
|
chmodSync4(dir2, 448);
|
|
7689
|
-
const target =
|
|
7969
|
+
const target = join14(dir2, basename(this.path));
|
|
7690
7970
|
copyFileSync(this.path, target);
|
|
7691
7971
|
chmodSync4(target, 384);
|
|
7692
7972
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -7732,7 +8012,7 @@ function pruneOld(dir2, retain) {
|
|
|
7732
8012
|
const drop = files.sort().slice(0, files.length - retain);
|
|
7733
8013
|
for (const f of drop) {
|
|
7734
8014
|
try {
|
|
7735
|
-
rmSync6(
|
|
8015
|
+
rmSync6(join14(dir2, f), { force: true });
|
|
7736
8016
|
} catch {
|
|
7737
8017
|
}
|
|
7738
8018
|
}
|
|
@@ -7879,6 +8159,84 @@ var TurnCommitter = class {
|
|
|
7879
8159
|
}
|
|
7880
8160
|
};
|
|
7881
8161
|
|
|
8162
|
+
// src/turn-runtime-integrity.ts
|
|
8163
|
+
function bundledHarnessFor(runtime) {
|
|
8164
|
+
return runtime === "claude-code" || runtime === "codex" ? runtime : null;
|
|
8165
|
+
}
|
|
8166
|
+
function resolve(ctx, harness) {
|
|
8167
|
+
return (ctx.resolveBundled ?? resolveBundledBinary)(harness);
|
|
8168
|
+
}
|
|
8169
|
+
async function postIncompleteNotice(ctx, harness, resolution) {
|
|
8170
|
+
try {
|
|
8171
|
+
await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
|
|
8172
|
+
body: turnIncompleteCopy(harness),
|
|
8173
|
+
kind: "final",
|
|
8174
|
+
turnId: ctx.turnId,
|
|
8175
|
+
parentMessageId: ctx.parentMessageId
|
|
8176
|
+
});
|
|
8177
|
+
return true;
|
|
8178
|
+
} catch (err) {
|
|
8179
|
+
ctx.log.warn(
|
|
8180
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8181
|
+
"dispatcher: runtime-incomplete notice post failed"
|
|
8182
|
+
);
|
|
8183
|
+
return false;
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
async function checkBundledBinary(ctx) {
|
|
8187
|
+
const harness = bundledHarnessFor(ctx.runtime);
|
|
8188
|
+
if (!harness) return null;
|
|
8189
|
+
const resolution = resolve(ctx, harness);
|
|
8190
|
+
if (resolution.status === "present") return null;
|
|
8191
|
+
ctx.log.error(
|
|
8192
|
+
{
|
|
8193
|
+
runtime: harness,
|
|
8194
|
+
status: resolution.status,
|
|
8195
|
+
platform: resolution.platform,
|
|
8196
|
+
arch: resolution.arch
|
|
8197
|
+
},
|
|
8198
|
+
"dispatcher: bundled harness binary missing at dispatch"
|
|
8199
|
+
);
|
|
8200
|
+
return {
|
|
8201
|
+
harness,
|
|
8202
|
+
reason: `runtime_incomplete:${harness}`,
|
|
8203
|
+
posted: await postIncompleteNotice(ctx, harness, resolution)
|
|
8204
|
+
};
|
|
8205
|
+
}
|
|
8206
|
+
async function reclassifyThrow(ctx, opts) {
|
|
8207
|
+
const harness = bundledHarnessFor(ctx.runtime);
|
|
8208
|
+
if (!harness || opts.aborted) return null;
|
|
8209
|
+
const resolution = resolve(ctx, harness);
|
|
8210
|
+
if (resolution.status === "present") return null;
|
|
8211
|
+
ctx.log.error(
|
|
8212
|
+
{
|
|
8213
|
+
runtime: harness,
|
|
8214
|
+
status: resolution.status,
|
|
8215
|
+
platform: resolution.platform,
|
|
8216
|
+
arch: resolution.arch,
|
|
8217
|
+
originalReason: opts.originalReason
|
|
8218
|
+
},
|
|
8219
|
+
"dispatcher: adapter threw and the bundled harness binary is missing \u2014 reporting the incomplete runtime"
|
|
8220
|
+
);
|
|
8221
|
+
return {
|
|
8222
|
+
harness,
|
|
8223
|
+
reason: `runtime_incomplete:${harness}`,
|
|
8224
|
+
posted: await postIncompleteNotice(ctx, harness, resolution)
|
|
8225
|
+
};
|
|
8226
|
+
}
|
|
8227
|
+
async function guardBundledBinary(ctx, concluded, outcome) {
|
|
8228
|
+
const incomplete = await checkBundledBinary(ctx);
|
|
8229
|
+
if (!incomplete) return;
|
|
8230
|
+
outcome.runtimeIncomplete = incomplete.posted;
|
|
8231
|
+
throw incomplete.posted ? concluded(incomplete.reason) : concluded(incomplete.reason, incomplete.reason);
|
|
8232
|
+
}
|
|
8233
|
+
async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
8234
|
+
const lost = await reclassifyThrow(ctx, { aborted, originalReason: outcome.resultReason });
|
|
8235
|
+
if (!lost) return false;
|
|
8236
|
+
outcome.resultReason = lost.reason;
|
|
8237
|
+
return lost.posted;
|
|
8238
|
+
}
|
|
8239
|
+
|
|
7882
8240
|
// src/turn-execution.ts
|
|
7883
8241
|
var PREPARING_TOOL_NAME = "preparing";
|
|
7884
8242
|
var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
|
|
@@ -7891,40 +8249,6 @@ var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
|
7891
8249
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7892
8250
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7893
8251
|
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
7894
|
-
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
7895
|
-
"dispatch_not_admitted",
|
|
7896
|
-
"turn_already_ended",
|
|
7897
|
-
"turn_belongs_elsewhere"
|
|
7898
|
-
]);
|
|
7899
|
-
function apiErrorCode(err) {
|
|
7900
|
-
if (!(err instanceof ApiError)) return null;
|
|
7901
|
-
const body = err.body;
|
|
7902
|
-
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
7903
|
-
}
|
|
7904
|
-
function leaseRefusal(err) {
|
|
7905
|
-
const code = apiErrorCode(err);
|
|
7906
|
-
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
7907
|
-
return null;
|
|
7908
|
-
}
|
|
7909
|
-
function isWriteFenceRefusal(err) {
|
|
7910
|
-
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
7911
|
-
}
|
|
7912
|
-
var ERROR_BODY_LOG_CAP = 2e3;
|
|
7913
|
-
function describeErrorBody(body) {
|
|
7914
|
-
if (body === void 0 || body === null) return void 0;
|
|
7915
|
-
let text;
|
|
7916
|
-
if (typeof body === "string") {
|
|
7917
|
-
text = body;
|
|
7918
|
-
} else {
|
|
7919
|
-
try {
|
|
7920
|
-
text = JSON.stringify(body);
|
|
7921
|
-
} catch {
|
|
7922
|
-
text = String(body);
|
|
7923
|
-
}
|
|
7924
|
-
}
|
|
7925
|
-
if (text.length === 0) return void 0;
|
|
7926
|
-
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7927
|
-
}
|
|
7928
8252
|
function initialOutcome() {
|
|
7929
8253
|
return {
|
|
7930
8254
|
sessionWritten: false,
|
|
@@ -7943,7 +8267,8 @@ function initialOutcome() {
|
|
|
7943
8267
|
settledDiagnostics: null,
|
|
7944
8268
|
silentMarkerEmitted: false,
|
|
7945
8269
|
timeoutReason: null,
|
|
7946
|
-
leaseLost: false
|
|
8270
|
+
leaseLost: false,
|
|
8271
|
+
runtimeIncomplete: false
|
|
7947
8272
|
};
|
|
7948
8273
|
}
|
|
7949
8274
|
var TurnConcluded = class {
|
|
@@ -8094,7 +8419,9 @@ var TurnExecution = class {
|
|
|
8094
8419
|
activeRunStartedAt: null,
|
|
8095
8420
|
turnId,
|
|
8096
8421
|
settledMessageId: payload.messageId,
|
|
8097
|
-
|
|
8422
|
+
// CT1379: `runtimeIncomplete` is a real failure that deliberately carries
|
|
8423
|
+
// no `errorReason`, so the inference alone would read it as settled.
|
|
8424
|
+
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
8098
8425
|
};
|
|
8099
8426
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
8100
8427
|
try {
|
|
@@ -8434,8 +8761,21 @@ ${reason}`,
|
|
|
8434
8761
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
8435
8762
|
});
|
|
8436
8763
|
}
|
|
8764
|
+
integrityContext() {
|
|
8765
|
+
return {
|
|
8766
|
+
runtime: this.turnContext.runtime,
|
|
8767
|
+
workspaceId: this.workspaceId,
|
|
8768
|
+
conversationId: this.payload.conversationId,
|
|
8769
|
+
turnId: this.turnId,
|
|
8770
|
+
parentMessageId: this.payload.messageId,
|
|
8771
|
+
log: this.turnLog,
|
|
8772
|
+
postTurnMessage: (workspaceId, conversationId, body) => this.opts.api.postTurnMessage(workspaceId, conversationId, body),
|
|
8773
|
+
...this.opts.resolveBundled ? { resolveBundled: this.opts.resolveBundled } : {}
|
|
8774
|
+
};
|
|
8775
|
+
}
|
|
8437
8776
|
async selectAdapter() {
|
|
8438
8777
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8778
|
+
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
8439
8779
|
const onWarn = (msg, meta) => turnLog.warn(meta ?? {}, msg);
|
|
8440
8780
|
const adapters = [];
|
|
8441
8781
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
@@ -8444,7 +8784,7 @@ ${reason}`,
|
|
|
8444
8784
|
if (this.opts.opencodeServerUrl) {
|
|
8445
8785
|
adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
|
|
8446
8786
|
}
|
|
8447
|
-
if (this.opts.
|
|
8787
|
+
if (this.opts.codexAvailable?.() ?? false) {
|
|
8448
8788
|
adapters.push(
|
|
8449
8789
|
createCodexAdapter({
|
|
8450
8790
|
enabled: true,
|
|
@@ -8728,6 +9068,11 @@ ${reason}`,
|
|
|
8728
9068
|
o.okResult = false;
|
|
8729
9069
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
8730
9070
|
turnLog.error({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
9071
|
+
o.runtimeIncomplete = await absorbBundledLoss(
|
|
9072
|
+
this.integrityContext(),
|
|
9073
|
+
abortController.signal.aborted,
|
|
9074
|
+
o
|
|
9075
|
+
);
|
|
8731
9076
|
} finally {
|
|
8732
9077
|
this.disarmWatchdogs();
|
|
8733
9078
|
if (o.leaseLost) {
|
|
@@ -8776,13 +9121,13 @@ ${reason}`,
|
|
|
8776
9121
|
if (o.turnResolvedConfig && Object.keys(o.turnResolvedConfig).length > 0) {
|
|
8777
9122
|
body.resolvedConfig = o.turnResolvedConfig;
|
|
8778
9123
|
}
|
|
8779
|
-
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !this.skipState.skipped) {
|
|
9124
|
+
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !o.runtimeIncomplete && !this.skipState.skipped) {
|
|
8780
9125
|
body.errorReason = o.resultReason.slice(0, 200);
|
|
8781
9126
|
}
|
|
8782
9127
|
if (o.okResult) {
|
|
8783
9128
|
body.lastSeenMessageId = payload.messageId;
|
|
8784
9129
|
}
|
|
8785
|
-
body.outcome = o.okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
9130
|
+
body.outcome = o.okResult ? "settled" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
8786
9131
|
if (o.sessionDegraded) {
|
|
8787
9132
|
body.degraded = true;
|
|
8788
9133
|
}
|
|
@@ -8907,15 +9252,15 @@ function checkoutState(cwd) {
|
|
|
8907
9252
|
if (entries.length === 0) {
|
|
8908
9253
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
8909
9254
|
}
|
|
8910
|
-
const gitPath =
|
|
9255
|
+
const gitPath = join15(cwd, ".git");
|
|
8911
9256
|
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
8912
9257
|
let stat;
|
|
8913
9258
|
try {
|
|
8914
|
-
stat =
|
|
9259
|
+
stat = statSync2(gitPath);
|
|
8915
9260
|
} catch (error) {
|
|
8916
9261
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
8917
9262
|
}
|
|
8918
|
-
if (stat.isDirectory() && !existsSync11(
|
|
9263
|
+
if (stat.isDirectory() && !existsSync11(join15(gitPath, "HEAD")))
|
|
8919
9264
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
8920
9265
|
return { ok: true, reason: "usable" };
|
|
8921
9266
|
}
|
|
@@ -9050,7 +9395,7 @@ import {
|
|
|
9050
9395
|
rmSync as rmSync7,
|
|
9051
9396
|
writeFileSync as writeFileSync7
|
|
9052
9397
|
} from "fs";
|
|
9053
|
-
import { join as
|
|
9398
|
+
import { join as join16 } from "path";
|
|
9054
9399
|
var MAX_ENTRIES = 2e3;
|
|
9055
9400
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
9056
9401
|
var Outbox = class {
|
|
@@ -9063,10 +9408,10 @@ var Outbox = class {
|
|
|
9063
9408
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
9064
9409
|
// cases route writes at the right tmpdir.
|
|
9065
9410
|
dir() {
|
|
9066
|
-
return
|
|
9411
|
+
return join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
9067
9412
|
}
|
|
9068
9413
|
fileFor(turnId, seq) {
|
|
9069
|
-
return
|
|
9414
|
+
return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
9070
9415
|
}
|
|
9071
9416
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
9072
9417
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -9108,7 +9453,7 @@ var Outbox = class {
|
|
|
9108
9453
|
const entries = [];
|
|
9109
9454
|
for (const name of names) {
|
|
9110
9455
|
if (!name.endsWith(".json")) continue;
|
|
9111
|
-
const full =
|
|
9456
|
+
const full = join16(dir2, name);
|
|
9112
9457
|
try {
|
|
9113
9458
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
9114
9459
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -9360,12 +9705,12 @@ var SseSubscriber = class {
|
|
|
9360
9705
|
}
|
|
9361
9706
|
};
|
|
9362
9707
|
function sleep2(ms) {
|
|
9363
|
-
return new Promise((
|
|
9708
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
9364
9709
|
}
|
|
9365
9710
|
|
|
9366
9711
|
// src/version.ts
|
|
9367
|
-
import { createRequire } from "module";
|
|
9368
|
-
var pkg =
|
|
9712
|
+
import { createRequire as createRequire2 } from "module";
|
|
9713
|
+
var pkg = createRequire2(import.meta.url)("../package.json");
|
|
9369
9714
|
var COMPANION_VERSION = pkg.version;
|
|
9370
9715
|
|
|
9371
9716
|
// src/supervisor.ts
|
|
@@ -9387,10 +9732,15 @@ var CompanionSupervisor = class {
|
|
|
9387
9732
|
config;
|
|
9388
9733
|
log;
|
|
9389
9734
|
hub;
|
|
9390
|
-
//
|
|
9391
|
-
//
|
|
9392
|
-
//
|
|
9393
|
-
|
|
9735
|
+
// CT1379: the bundled-executable resolver — the execution-presence half of both
|
|
9736
|
+
// routing gates. Synchronous and cheap (a module resolve plus a stat), so unlike
|
|
9737
|
+
// the CLI probes it needs no boot-frozen fallback: a caller that asks before the
|
|
9738
|
+
// first harness refresh simply resolves now.
|
|
9739
|
+
resolveBundled;
|
|
9740
|
+
// CT1379: the last bundled status LOGGED per harness, so a missing binary is
|
|
9741
|
+
// reported at startup and on each healthy↔missing transition rather than on
|
|
9742
|
+
// every 30s beat (the CT484 latch pattern, per runtime).
|
|
9743
|
+
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
9394
9744
|
// CT571: the probed per-harness versions, reported on each heartbeat manifest.
|
|
9395
9745
|
// CT586: no longer boot-frozen — the per-heartbeat harness re-probe refreshes
|
|
9396
9746
|
// it, so a harness updated mid-run reports its new version without a restart.
|
|
@@ -9434,13 +9784,14 @@ var CompanionSupervisor = class {
|
|
|
9434
9784
|
this.config = opts.config;
|
|
9435
9785
|
this.log = opts.log;
|
|
9436
9786
|
this.hub = opts.hub;
|
|
9437
|
-
this.
|
|
9787
|
+
this.resolveBundled = opts.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
|
|
9438
9788
|
this.probePresence = opts.probePresence ?? probeHarnessPresence;
|
|
9439
9789
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
9440
9790
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
9441
9791
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
9442
9792
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
9443
9793
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
9794
|
+
this.latchBootBundledState();
|
|
9444
9795
|
}
|
|
9445
9796
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
9446
9797
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
@@ -9523,19 +9874,25 @@ var CompanionSupervisor = class {
|
|
|
9523
9874
|
const res = await this.deviceApi.heartbeat({
|
|
9524
9875
|
version: COMPANION_VERSION,
|
|
9525
9876
|
exposedSecretNames: store.names(),
|
|
9526
|
-
// Report each runtime only when this device can actually run it:
|
|
9527
|
-
// claude-code
|
|
9877
|
+
// Report each runtime only when this device can actually run it:
|
|
9878
|
+
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
9879
|
+
// and the user asked for the harness, CT270 opencode when the operator
|
|
9528
9880
|
// configured an `opencode serve`.
|
|
9529
9881
|
manifest: buildCompanionManifest({
|
|
9530
|
-
// CT1082: connected AND
|
|
9531
|
-
//
|
|
9532
|
-
// user's `claudeCode.enabled`. Claude Code
|
|
9882
|
+
// CT1082/CT1379: connected AND launchable. Execution presence is the
|
|
9883
|
+
// live bundled-binary resolution (refreshed on this beat), consent is
|
|
9884
|
+
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
9533
9885
|
claudeCode: this.claudeCodeOffered(),
|
|
9534
9886
|
opencode: !!this.config.opencode?.serverUrl,
|
|
9535
|
-
// CT481: advertise codex when the operator enabled it
|
|
9536
|
-
//
|
|
9537
|
-
// a
|
|
9538
|
-
|
|
9887
|
+
// CT481/CT1379: advertise codex when the operator enabled it AND its
|
|
9888
|
+
// vendored binary is there. The enable flag alone used to be enough,
|
|
9889
|
+
// which is how a device with a half-installed 331 MB platform package
|
|
9890
|
+
// advertised a runtime that could not start.
|
|
9891
|
+
codex: this.codexOffered(),
|
|
9892
|
+
// CT1379: and say WHY when an asked-for harness is absent from the list
|
|
9893
|
+
// above. Diagnostic only — the server never routes on it — and always
|
|
9894
|
+
// sent, so an empty array clears a previously reported issue.
|
|
9895
|
+
runtimeIssues: this.runtimeIssues(),
|
|
9539
9896
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
9540
9897
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
9541
9898
|
versions: this.harnessVersions
|
|
@@ -9756,18 +10113,103 @@ var CompanionSupervisor = class {
|
|
|
9756
10113
|
"companion: stopped agent (unassigned)"
|
|
9757
10114
|
);
|
|
9758
10115
|
}
|
|
9759
|
-
//
|
|
9760
|
-
//
|
|
9761
|
-
//
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
//
|
|
9768
|
-
//
|
|
10116
|
+
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
10117
|
+
// executable there? Live (the per-beat re-resolve), falling back to resolving
|
|
10118
|
+
// right now before the first refresh lands. Execution presence ONLY; consent is
|
|
10119
|
+
// the caller's half.
|
|
10120
|
+
bundledStatus(runtime) {
|
|
10121
|
+
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
10122
|
+
return cached2 ?? this.resolveBundled(runtime).status;
|
|
10123
|
+
}
|
|
10124
|
+
// The full resolution rather than just its status — the transition log needs the
|
|
10125
|
+
// platform/architecture and the repair the copy is built from, and re-resolving
|
|
10126
|
+
// is a module lookup plus a stat.
|
|
10127
|
+
bundledResolution(runtime) {
|
|
10128
|
+
return this.resolveBundled(runtime);
|
|
10129
|
+
}
|
|
10130
|
+
// CT1082/CT1379: does this device OFFER claude-code — connected by its user, and
|
|
10131
|
+
// able to start the binary a turn runs? The ONE signal both the heartbeat
|
|
10132
|
+
// manifest and the dispatcher's adapter registry read, so what the device
|
|
10133
|
+
// advertises and what it can select can't disagree (the CT833 invariant, with
|
|
10134
|
+
// consent in front of it and the real executable underneath it).
|
|
9769
10135
|
claudeCodeOffered() {
|
|
9770
|
-
return isClaudeCodeConnected(this.config) && this.
|
|
10136
|
+
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
10137
|
+
}
|
|
10138
|
+
// CT1379: and the same question for codex, which until now was the enable flag
|
|
10139
|
+
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
10140
|
+
// could not start. Same shape as Claude Code, same predicate on both sides.
|
|
10141
|
+
codexOffered() {
|
|
10142
|
+
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
10143
|
+
}
|
|
10144
|
+
// CT1379: the manifest's diagnostic half, derived from the same two facts as the
|
|
10145
|
+
// two predicates above so an issue and an advertisement can never both be made
|
|
10146
|
+
// for one harness.
|
|
10147
|
+
runtimeIssues() {
|
|
10148
|
+
return runtimeIssuesFor({
|
|
10149
|
+
claudeCode: {
|
|
10150
|
+
intended: isClaudeCodeConnected(this.config),
|
|
10151
|
+
bundled: this.bundledStatus("claude-code")
|
|
10152
|
+
},
|
|
10153
|
+
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
10154
|
+
});
|
|
10155
|
+
}
|
|
10156
|
+
// CT1379: a missing binary is reported ONCE, and again only when the answer
|
|
10157
|
+
// CHANGES — with the runtime, the platform/architecture and the resolution
|
|
10158
|
+
// result, which is enough to find the occurrence in `companion.log` without a
|
|
10159
|
+
// repro and carries nothing that could be a credential or a vendor's output.
|
|
10160
|
+
//
|
|
10161
|
+
// Two things this deliberately does NOT do (Codo's review, blocking finding #2):
|
|
10162
|
+
//
|
|
10163
|
+
// - It does not re-announce the state the device booted in. `warnAboutHarnessReadiness`
|
|
10164
|
+
// already said that on stderr at startup, in the specified words, at the one
|
|
10165
|
+
// moment a person is watching a terminal — and the `--daemon` launcher runs
|
|
10166
|
+
// that preflight in the FOREGROUND for exactly that reason, because the
|
|
10167
|
+
// child's output goes to a file. The boot state is latched in the
|
|
10168
|
+
// constructor, so the first refresh reports nothing new; it lands here as a
|
|
10169
|
+
// debug record instead, present for diagnosis without competing for
|
|
10170
|
+
// attention with the warning the operator already read.
|
|
10171
|
+
// - It does not report a transition in generic prose. A binary that goes
|
|
10172
|
+
// missing at 3am is the case the operator will actually meet, and a line
|
|
10173
|
+
// that says only "binary is missing" leaves them with no repair and no
|
|
10174
|
+
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
10175
|
+
// same sentence the startup path prints.
|
|
10176
|
+
logBundledTransitions() {
|
|
10177
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
10178
|
+
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
10179
|
+
const resolution = this.bundledResolution(runtime);
|
|
10180
|
+
const status = resolution.status;
|
|
10181
|
+
if (this.loggedBundledStatus.get(runtime) === status) continue;
|
|
10182
|
+
this.loggedBundledStatus.set(runtime, status);
|
|
10183
|
+
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status };
|
|
10184
|
+
if (status === "present") {
|
|
10185
|
+
const say = intended ? this.log.info : this.log.debug;
|
|
10186
|
+
say.call(this.log, meta, "companion: bundled harness binary is present again");
|
|
10187
|
+
continue;
|
|
10188
|
+
}
|
|
10189
|
+
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
10190
|
+
else
|
|
10191
|
+
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
10192
|
+
}
|
|
10193
|
+
}
|
|
10194
|
+
// CT1379: the boot state, latched so the first refresh reports no news, and
|
|
10195
|
+
// recorded once as evidence. Runs in the constructor — the resolve is a module
|
|
10196
|
+
// lookup plus a stat, so it costs nothing and needs no await.
|
|
10197
|
+
latchBootBundledState() {
|
|
10198
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
10199
|
+
const resolution = this.resolveBundled(runtime);
|
|
10200
|
+
this.loggedBundledStatus.set(runtime, resolution.status);
|
|
10201
|
+
if (resolution.status === "present") continue;
|
|
10202
|
+
this.log.debug(
|
|
10203
|
+
{
|
|
10204
|
+
runtime,
|
|
10205
|
+
platform: resolution.platform,
|
|
10206
|
+
arch: resolution.arch,
|
|
10207
|
+
status: resolution.status,
|
|
10208
|
+
intended: runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config)
|
|
10209
|
+
},
|
|
10210
|
+
"companion: bundled harness binary missing at startup (warned by the readiness check)"
|
|
10211
|
+
);
|
|
10212
|
+
}
|
|
9771
10213
|
}
|
|
9772
10214
|
buildDispatcher(ctx) {
|
|
9773
10215
|
const local = localAgentConfig(this.config, {
|
|
@@ -9791,16 +10233,23 @@ var CompanionSupervisor = class {
|
|
|
9791
10233
|
// CT833: register the claude-code adapter only when this device actually
|
|
9792
10234
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
9793
10235
|
// installed or connected after boot works on the next turn exactly as it
|
|
9794
|
-
// appears on the next beat. CT1082: "offers"
|
|
9795
|
-
//
|
|
10236
|
+
// appears on the next beat. CT1082: "offers" means connected as well as
|
|
10237
|
+
// runnable, so a disconnected harness can't be selected either.
|
|
10238
|
+
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
10239
|
+
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
10240
|
+
// other is not expressible.
|
|
9796
10241
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
9797
10242
|
// CT270: the opencode server URL (operator-configured), when this device
|
|
9798
10243
|
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
9799
10244
|
// opencode adapter; unset leaves the device claude-code-only.
|
|
9800
10245
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
9801
|
-
// CT481: register the codex adapter when this device offers codex;
|
|
10246
|
+
// CT481: register the codex adapter when this device offers codex; a miss
|
|
9802
10247
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
9803
|
-
|
|
10248
|
+
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
10249
|
+
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
10250
|
+
// from the enable flag alone, so a device could register an adapter for a
|
|
10251
|
+
// binary it did not have and keep it registered after the binary came back.
|
|
10252
|
+
codexAvailable: () => this.codexOffered(),
|
|
9804
10253
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
9805
10254
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
9806
10255
|
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
@@ -10170,9 +10619,13 @@ var CompanionSupervisor = class {
|
|
|
10170
10619
|
try {
|
|
10171
10620
|
const signals = await probeHarnessSignals(this.config, {
|
|
10172
10621
|
probePresence: this.probePresence,
|
|
10173
|
-
presence
|
|
10622
|
+
presence,
|
|
10623
|
+
// CT1379: the bundled executables are resolved on the same cadence as the
|
|
10624
|
+
// CLI probes, through the supervisor's seam so a test pins the host fact.
|
|
10625
|
+
resolveBundled: this.resolveBundled
|
|
10174
10626
|
});
|
|
10175
10627
|
this.harnessSignals = signals;
|
|
10628
|
+
this.logBundledTransitions();
|
|
10176
10629
|
this.harnessVersions = {
|
|
10177
10630
|
claudeCode: signals.claudeVersion,
|
|
10178
10631
|
opencode: signals.opencodeVersion,
|
|
@@ -10212,8 +10665,15 @@ var CompanionSupervisor = class {
|
|
|
10212
10665
|
return this.config;
|
|
10213
10666
|
}
|
|
10214
10667
|
// Friendly enable for the config-driven harnesses — flip the flag the app owns in
|
|
10215
|
-
// `~/.cabane/config.json`, no hand-edited JSON.
|
|
10216
|
-
// never
|
|
10668
|
+
// `~/.cabane/config.json`, no hand-edited JSON. It never installs a binary and
|
|
10669
|
+
// never drives a login (BYO — Decided).
|
|
10670
|
+
//
|
|
10671
|
+
// CT1379: this records INTENT; it does not by itself expose anything. A harness
|
|
10672
|
+
// is advertised only when the user asked for it AND the SDK's own bundled binary
|
|
10673
|
+
// resolves, so a successful enable on a companion whose optional dependency
|
|
10674
|
+
// never installed leaves the device advertising nothing — correctly, and with
|
|
10675
|
+
// the Harnesses surface saying why. Read every "expose" below as "record the
|
|
10676
|
+
// half of the answer the user owns".
|
|
10217
10677
|
// - claude-code (CT1082): set `claudeCode.enabled`, after checking `claude` is
|
|
10218
10678
|
// on PATH. This is the callable the terminal offer ("We found Claude Code.
|
|
10219
10679
|
// Connect it? [Y/n]") and the web pairing flow both wire to.
|
|
@@ -10222,9 +10682,10 @@ var CompanionSupervisor = class {
|
|
|
10222
10682
|
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
10223
10683
|
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
10224
10684
|
// typed failure the UI shows in place, not a silent bad write.
|
|
10225
|
-
// On success, persist, re-probe, and beat immediately so
|
|
10226
|
-
//
|
|
10227
|
-
//
|
|
10685
|
+
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
10686
|
+
// launchable starts taking turns at once rather than on the next tick — and one
|
|
10687
|
+
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
10688
|
+
// throws for a reachable/valid input).
|
|
10228
10689
|
async enableHarness(input) {
|
|
10229
10690
|
let next;
|
|
10230
10691
|
let checkedPresence = null;
|
|
@@ -10322,8 +10783,8 @@ var CompanionSupervisor = class {
|
|
|
10322
10783
|
};
|
|
10323
10784
|
async function waitForBoundedHeartbeat(heartbeat) {
|
|
10324
10785
|
let timer = null;
|
|
10325
|
-
const timeout = new Promise((
|
|
10326
|
-
timer = setTimeout(
|
|
10786
|
+
const timeout = new Promise((resolve2) => {
|
|
10787
|
+
timer = setTimeout(resolve2, SHUTDOWN_HEARTBEAT_WAIT_MS);
|
|
10327
10788
|
timer.unref?.();
|
|
10328
10789
|
});
|
|
10329
10790
|
try {
|
|
@@ -10407,9 +10868,9 @@ function handleUncaught(log, err, origin) {
|
|
|
10407
10868
|
|
|
10408
10869
|
// src/crash-marker.ts
|
|
10409
10870
|
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10410
|
-
import { join as
|
|
10871
|
+
import { join as join17 } from "path";
|
|
10411
10872
|
function crashMarkerPath() {
|
|
10412
|
-
return
|
|
10873
|
+
return join17(cabaneDir(), "last-error.json");
|
|
10413
10874
|
}
|
|
10414
10875
|
function recordCrash(rec) {
|
|
10415
10876
|
try {
|
|
@@ -10458,8 +10919,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10458
10919
|
}
|
|
10459
10920
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
10460
10921
|
const harnessVersions = await probeHarnessVersions({
|
|
10461
|
-
//
|
|
10462
|
-
// claude
|
|
10922
|
+
// Connected AND its CLI answered — the two things that make a
|
|
10923
|
+
// `claude --version` probe worth spawning. Not the offer predicate.
|
|
10463
10924
|
claudeCode: claudeCode && isClaudeCodeConnected(cfg),
|
|
10464
10925
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
10465
10926
|
codex: isCodexEnabled(cfg)
|
|
@@ -10492,7 +10953,6 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10492
10953
|
config: cfg,
|
|
10493
10954
|
log,
|
|
10494
10955
|
hub,
|
|
10495
|
-
claudeCode,
|
|
10496
10956
|
harnessVersions
|
|
10497
10957
|
});
|
|
10498
10958
|
await supervisor.start();
|