@cabane/companion 0.6.35 → 0.6.37

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.
Files changed (4) hide show
  1. package/README.md +128 -36
  2. package/dist/cli.js +416 -789
  3. package/dist/runtime.js +274 -119
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -806,8 +806,12 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
806
806
  ...claudeInstalled ? ["Claude Code"] : [],
807
807
  ...codexInstalled ? ["Codex"] : []
808
808
  ];
809
+ const connectCommands = [
810
+ ...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
811
+ ...codexInstalled ? ["`cabane-companion connect codex`"] : []
812
+ ];
809
813
  warn(
810
- "No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"} in the Companion dashboard (or in cabane, Settings \u2192 Devices) and turns start routing here.` : "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 in the Companion dashboard.")
814
+ "No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one with " : "it with "}${connectCommands.join(" or ")} and turns start routing here.` : "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).")
811
815
  );
812
816
  }
813
817
 
@@ -1663,30 +1667,10 @@ async function pair(opts = {}) {
1663
1667
  }
1664
1668
 
1665
1669
  // src/cli.ts
1666
- import { readFileSync as readFileSync12 } from "fs";
1667
-
1668
- // src/service/index.ts
1669
- import { dirname as dirname5 } from "path";
1670
+ import { readFileSync as readFileSync11 } from "fs";
1670
1671
 
1671
- // src/service/host.ts
1672
- import { spawnSync } from "child_process";
1673
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "fs";
1674
- import { homedir as homedir3 } from "os";
1675
-
1676
- // src/cli-entry.ts
1677
- import { existsSync as existsSync5 } from "fs";
1678
- import { fileURLToPath } from "url";
1679
- var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
1680
- function companionCliEntry(deps = {}) {
1681
- const exists = deps.exists ?? existsSync5;
1682
- const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
1683
- for (const candidate of candidates) {
1684
- if (exists(candidate)) return candidate;
1685
- }
1686
- const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
1687
- if (argv1 && exists(argv1)) return argv1;
1688
- return candidates[0] ?? "";
1689
- }
1672
+ // src/commands/start.ts
1673
+ import { hostname as hostname2 } from "os";
1690
1674
 
1691
1675
  // src/logger.ts
1692
1676
  import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
@@ -1752,455 +1736,19 @@ function getLogger() {
1752
1736
  return cached;
1753
1737
  }
1754
1738
 
1755
- // src/service/host.ts
1756
- var RUN_TIMEOUT_MS = 1e4;
1757
- function defaultServiceHost() {
1758
- if (process.env.VITEST) {
1759
- throw new Error(
1760
- "defaultServiceHost() was reached during a test run \u2014 it shells out to the real service manager. Inject a fake host (apps/companion/test/service-host-fake.ts) instead."
1761
- );
1762
- }
1763
- return {
1764
- platform: process.platform,
1765
- env: process.env,
1766
- home: homedir3(),
1767
- uid: process.getuid?.() ?? 0,
1768
- execPath: process.execPath,
1769
- cliPath: companionCliEntry(),
1770
- logPath: companionLogPath(),
1771
- fs: {
1772
- read: (path) => {
1773
- try {
1774
- return readFileSync4(path, "utf8");
1775
- } catch {
1776
- return null;
1777
- }
1778
- },
1779
- write: (path, contents) => writeFileSync4(path, contents, "utf8"),
1780
- remove: (path) => rmSync5(path, { force: true }),
1781
- exists: (path) => existsSync6(path),
1782
- mkdirp: (dir2) => {
1783
- mkdirSync6(dir2, { recursive: true });
1784
- }
1785
- },
1786
- run: (cmd, args) => {
1787
- const res = spawnSync(cmd, args, { encoding: "utf8", timeout: RUN_TIMEOUT_MS });
1788
- return {
1789
- ok: res.status === 0,
1790
- stdout: res.stdout ?? "",
1791
- stderr: res.stderr ?? (res.error ? res.error.message : "")
1792
- };
1793
- }
1794
- };
1795
- }
1796
-
1797
- // src/service/launchd.ts
1798
- import { join as join6 } from "path";
1799
- var LAUNCHD_LABEL = "ai.cabane.companion";
1800
- function launchAgentPath(home) {
1801
- return join6(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
1802
- }
1803
- function renderPlist(input) {
1804
- const args = input.programArguments.map((a) => ` <string>${xml(a)}</string>`).join("\n");
1805
- const env = Object.entries(input.environment).map(([k, v]) => ` <key>${xml(k)}</key>
1806
- <string>${xml(v)}</string>`).join("\n");
1807
- return [
1808
- '<?xml version="1.0" encoding="UTF-8"?>',
1809
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
1810
- '<plist version="1.0">',
1811
- "<dict>",
1812
- " <key>Label</key>",
1813
- ` <string>${LAUNCHD_LABEL}</string>`,
1814
- " <key>ProgramArguments</key>",
1815
- " <array>",
1816
- args,
1817
- " </array>",
1818
- " <key>RunAtLoad</key>",
1819
- " <true/>",
1820
- " <key>KeepAlive</key>",
1821
- " <dict>",
1822
- " <key>SuccessfulExit</key>",
1823
- " <false/>",
1824
- " </dict>",
1825
- " <key>EnvironmentVariables</key>",
1826
- " <dict>",
1827
- env,
1828
- " </dict>",
1829
- " <key>StandardOutPath</key>",
1830
- ` <string>${xml(input.logPath)}</string>`,
1831
- " <key>StandardErrorPath</key>",
1832
- ` <string>${xml(input.logPath)}</string>`,
1833
- "</dict>",
1834
- "</plist>",
1835
- ""
1836
- ].join("\n");
1837
- }
1838
- function domain(host) {
1839
- return `gui/${host.uid}`;
1840
- }
1841
- function target(host) {
1842
- return `${domain(host)}/${LAUNCHD_LABEL}`;
1843
- }
1844
- function launchdStart(host, plistPath) {
1845
- host.run("launchctl", ["bootout", target(host)]);
1846
- const res = host.run("launchctl", ["bootstrap", domain(host), plistPath]);
1847
- return res.ok ? { ok: true } : { ok: false, detail: firstLine(res.stderr || res.stdout) };
1848
- }
1849
- function launchdStop(host) {
1850
- const res = host.run("launchctl", ["bootout", target(host)]);
1851
- if (res.ok || notLoaded(res.stderr + res.stdout)) return { ok: true };
1852
- return { ok: false, detail: firstLine(res.stderr || res.stdout) };
1853
- }
1854
- function launchdProbe(host) {
1855
- const res = host.run("launchctl", ["print", target(host)]);
1856
- if (res.ok) {
1857
- const state = /state\s*=\s*(\w+)/.exec(res.stdout)?.[1];
1858
- return { ownership: "held", running: state ? state === "running" : "unknown" };
1859
- }
1860
- return notLoaded(res.stderr + res.stdout) ? { ownership: "clear", running: false } : { ownership: "unknown", running: "unknown" };
1861
- }
1862
- function launchdRemove(host, plistPath) {
1863
- const stopped = launchdStop(host);
1864
- if (!stopped.ok) return stopped;
1865
- host.fs.remove(plistPath);
1866
- return { ok: true };
1867
- }
1868
- var NOT_LOADED = new RegExp(
1869
- `no such process|(could not find|not find service)[^\\n]*${LAUNCHD_LABEL.replace(
1870
- /[.*+?^${}()|[\]\\]/g,
1871
- "\\$&"
1872
- )}`,
1873
- "i"
1874
- );
1875
- function notLoaded(output) {
1876
- return NOT_LOADED.test(output);
1877
- }
1878
- function firstLine(s) {
1879
- return s.trim().split("\n")[0] ?? "";
1880
- }
1881
- function xml(value) {
1882
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1883
- }
1884
-
1885
- // src/service/systemd.ts
1886
- import { dirname as dirname4, join as join7 } from "path";
1887
- var SYSTEMD_UNIT = "cabane-companion.service";
1888
- function systemdUnitPath(host) {
1889
- const configHome = host.env.XDG_CONFIG_HOME?.trim() ? host.env.XDG_CONFIG_HOME.trim() : join7(host.home, ".config");
1890
- return join7(configHome, "systemd", "user", SYSTEMD_UNIT);
1891
- }
1892
- function renderUnit(input) {
1893
- const [exec, ...rest] = input.programArguments;
1894
- const execStart = [quote(exec ?? ""), ...rest.map(quote)].join(" ");
1895
- const env = Object.entries(input.environment).map(([k, v]) => `Environment=${k}=${v}`);
1896
- return [
1897
- "[Unit]",
1898
- "Description=Cabane Companion \u2014 keeps this device answering while you are logged in",
1899
- "After=network-online.target",
1900
- "Wants=network-online.target",
1901
- "",
1902
- "[Service]",
1903
- "Type=simple",
1904
- ...env,
1905
- `ExecStart=${execStart}`,
1906
- "Restart=on-failure",
1907
- "RestartSec=5",
1908
- "",
1909
- "[Install]",
1910
- "WantedBy=default.target"
1911
- ].join("\n") + "\n";
1912
- }
1913
- function systemdReload(host) {
1914
- host.run("systemctl", ["--user", "daemon-reload"]);
1915
- }
1916
- function systemdStart(host) {
1917
- systemdReload(host);
1918
- const enabled = host.run("systemctl", ["--user", "enable", SYSTEMD_UNIT]);
1919
- if (!enabled.ok) return { ok: false, detail: firstLine2(enabled.stderr || enabled.stdout) };
1920
- const started = host.run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
1921
- if (!started.ok) return { ok: false, detail: firstLine2(started.stderr || started.stdout) };
1922
- return { ok: true };
1923
- }
1924
- function systemdStop(host) {
1925
- const res = host.run("systemctl", ["--user", "stop", SYSTEMD_UNIT]);
1926
- if (res.ok || notLoaded2(res.stderr + res.stdout)) return { ok: true };
1927
- return { ok: false, detail: firstLine2(res.stderr || res.stdout) };
1928
- }
1929
- function systemdProbe(host) {
1930
- const state = host.run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]).stdout.trim();
1931
- if (state === "active") return { ownership: "held", running: true };
1932
- if (state === "activating" || state === "reloading" || state === "deactivating") {
1933
- return { ownership: "held", running: "transitional" };
1934
- }
1935
- if (state === "inactive" || state === "failed") return { ownership: "clear", running: false };
1936
- return { ownership: "unknown", running: "unknown" };
1937
- }
1938
- function systemdWantsLinkPath(host) {
1939
- return join7(dirname4(systemdUnitPath(host)), "default.target.wants", SYSTEMD_UNIT);
1940
- }
1941
- function systemdInstalled(host) {
1942
- return host.fs.exists(systemdUnitPath(host)) || host.fs.exists(systemdWantsLinkPath(host));
1943
- }
1944
- function systemdRemove(host, unitPath) {
1945
- const stopped = host.run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
1946
- if (!(stopped.ok || notLoaded2(stopped.stderr + stopped.stdout))) {
1947
- return { ok: false, detail: firstLine2(stopped.stderr || stopped.stdout) };
1948
- }
1949
- host.fs.remove(unitPath);
1950
- const link = systemdWantsLinkPath(host);
1951
- if (host.fs.exists(link)) host.fs.remove(link);
1952
- systemdReload(host);
1953
- return { ok: true };
1954
- }
1955
- var NOT_LOADED2 = new RegExp(
1956
- `unit (file )?${SYSTEMD_UNIT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (not loaded|does not exist|not found|could not be found)`,
1957
- "i"
1958
- );
1959
- function notLoaded2(output) {
1960
- return NOT_LOADED2.test(output);
1961
- }
1962
- function readLinger(host) {
1963
- const res = host.run("loginctl", ["show-user", String(host.uid), "-p", "Linger"]);
1964
- if (!res.ok) return "unknown";
1965
- const match = /Linger=(\w+)/.exec(res.stdout);
1966
- if (!match) return "unknown";
1967
- return match[1] === "yes" ? "yes" : "no";
1968
- }
1969
- function tryEnableLinger(host) {
1970
- host.run("loginctl", ["enable-linger", String(host.uid)]);
1971
- return readLinger(host);
1972
- }
1973
- function isRemoteSession(env) {
1974
- return Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
1975
- }
1976
- function quote(value) {
1977
- return `"${value.replace(/(["\\])/g, "\\$1")}"`;
1978
- }
1979
- function firstLine2(s) {
1980
- return s.trim().split("\n")[0] ?? "";
1981
- }
1982
-
1983
- // src/service/index.ts
1984
- function detectServiceManager(host = defaultServiceHost()) {
1985
- if (host.platform === "darwin") return "launchd";
1986
- if (host.platform !== "linux") return "none";
1987
- return host.run("systemctl", ["--user", "show-environment"]).ok ? "systemd-user" : "none";
1988
- }
1989
- function programArguments(host, _opts) {
1990
- return [host.execPath, host.cliPath, "start", "--foreground"];
1991
- }
1992
- function environment(host) {
1993
- return {
1994
- // PATH CAPTURE. launchd's default PATH is `/usr/bin:/bin:/usr/sbin:/sbin`
1995
- // and a systemd user manager's is barely better — neither has `claude`,
1996
- // `codex` or `opencode` on it. A companion that runs but can't find its
1997
- // harness is worse than one that isn't running, because the device reads
1998
- // Online. So we bake in the PATH of the shell that ran `start`, and because
1999
- // it's part of the rendered content, a PATH that later drifts re-renders on
2000
- // the next `start` like any other change.
2001
- PATH: host.env.PATH ?? "",
2002
- CABANE_COMPANION_DAEMON: "1"
2003
- };
2004
- }
2005
- function unitPathFor(host, manager) {
2006
- if (manager === "launchd") return launchAgentPath(host.home);
2007
- if (manager === "systemd-user") return systemdUnitPath(host);
2008
- return null;
2009
- }
2010
- function renderFor(host, manager, opts) {
2011
- const input = { programArguments: programArguments(host, opts), environment: environment(host) };
2012
- return manager === "launchd" ? renderPlist({ ...input, logPath: host.logPath }) : renderUnit(input);
2013
- }
2014
- function installService(opts = {}, host = defaultServiceHost()) {
2015
- const manager = detectServiceManager(host);
2016
- if (manager === "none") return { installed: false, reason: "unsupported" };
2017
- const path = unitPathFor(host, manager);
2018
- if (!path) return { installed: false, reason: "unsupported" };
2019
- const desired = renderFor(host, manager, opts);
2020
- const existing = host.fs.read(path);
2021
- const hadUnit = existing !== null;
2022
- const changed = existing !== desired;
2023
- if (changed) {
2024
- host.fs.mkdirp(dirname5(path));
2025
- host.fs.write(path, desired);
2026
- }
2027
- if (manager === "systemd-user") {
2028
- const linger = ensureLinger(host);
2029
- if (linger !== "yes" && isRemoteSession(host.env)) {
2030
- systemdRemove(host, path);
2031
- return failedInstall(
2032
- host,
2033
- manager,
2034
- "linger-unavailable",
2035
- "a systemd --user service would stop when this SSH session ends (lingering is off and could not be enabled)"
2036
- );
2037
- }
2038
- }
2039
- const started = manager === "launchd" ? launchdStart(host, path) : systemdStart(host);
2040
- if (!started.ok) {
2041
- if (!hadUnit) removeService(host, manager, path);
2042
- return failedInstall(host, manager, "command-failed", started.detail);
2043
- }
2044
- return { installed: true, manager, changed };
2045
- }
2046
- function failedInstall(host, manager, reason, detail) {
2047
- return {
2048
- installed: false,
2049
- reason,
2050
- ...detail ? { detail } : {},
2051
- ...managerOwnership(host, manager) === "clear" ? {} : { leftBehind: manager }
2052
- };
2053
- }
2054
- function managerProbe(host, manager) {
2055
- if (manager === "launchd") return launchdProbe(host);
2056
- if (manager === "systemd-user") return systemdProbe(host);
2057
- return { ownership: "clear", running: false };
2058
- }
2059
- function managerOwnership(host, manager) {
2060
- return managerProbe(host, manager).ownership;
2061
- }
2062
- function refreshInstalledService(opts = {}, host = defaultServiceHost()) {
2063
- const manager = detectServiceManager(host);
2064
- const path = unitPathFor(host, manager);
2065
- if (!path || !host.fs.exists(path)) return false;
2066
- const desired = renderFor(host, manager, opts);
2067
- if (host.fs.read(path) === desired) return false;
2068
- host.fs.write(path, desired);
2069
- if (manager === "systemd-user") systemdReload(host);
2070
- return true;
2071
- }
2072
- function stopService(host = defaultServiceHost()) {
2073
- const manager = detectServiceManager(host);
2074
- const path = unitPathFor(host, manager);
2075
- if (manager === "none" || !path || !managerHasClaim(host, manager, path)) {
2076
- return { handled: false, ok: true };
2077
- }
2078
- const res = manager === "launchd" ? launchdStop(host) : systemdStop(host);
2079
- return {
2080
- handled: true,
2081
- ok: res.ok,
2082
- manager,
2083
- ...res.detail ? { detail: res.detail } : {}
2084
- };
2085
- }
2086
- function disableService(host = defaultServiceHost()) {
2087
- const manager = detectServiceManager(host);
2088
- const path = unitPathFor(host, manager);
2089
- if (manager === "none" || !path) return { handled: false, ok: true };
2090
- if (!managerHasClaim(host, manager, path)) return { handled: false, ok: true, manager };
2091
- const res = removeService(host, manager, path);
2092
- return { handled: true, ok: res.ok, manager, ...res.detail ? { detail: res.detail } : {} };
2093
- }
2094
- function managerHasClaim(host, manager, path) {
2095
- if (manager === "none") return false;
2096
- if (host.fs.exists(path)) return true;
2097
- if (manager === "systemd-user" && systemdInstalled(host)) return true;
2098
- return managerOwnership(host, manager) !== "clear";
2099
- }
2100
- function removeService(host, manager, path) {
2101
- return manager === "launchd" ? launchdRemove(host, path) : systemdRemove(host, path);
2102
- }
2103
- function serviceStatus(host = defaultServiceHost()) {
2104
- const manager = detectServiceManager(host);
2105
- const path = unitPathFor(host, manager);
2106
- const probe = managerProbe(host, manager);
2107
- const installed = manager === "none" || !path ? false : host.fs.exists(path) || probe.ownership === "held" || manager === "systemd-user" && systemdInstalled(host);
2108
- const running = manager === "none" ? false : probe.running;
2109
- return {
2110
- manager,
2111
- unitPath: path,
2112
- installed,
2113
- running,
2114
- linger: manager === "systemd-user" ? readLinger(host) : null,
2115
- remoteSession: isRemoteSession(host.env)
2116
- };
2117
- }
2118
- function ensureLinger(host) {
2119
- const current = readLinger(host);
2120
- if (current === "yes") return current;
2121
- return tryEnableLinger(host);
2122
- }
2123
-
2124
- // src/commands/service.ts
2125
- function serviceStatusCommand(deps = {}) {
2126
- const status2 = (deps.status ?? serviceStatus)();
2127
- if (status2.manager === "none") {
2128
- process.stdout.write(
2129
- "service: not supported on this machine yet \u2014 `cabane-companion start` runs the companion detached instead (it stops at the next reboot).\n"
2130
- );
2131
- return;
2132
- }
2133
- process.stdout.write(`manager: ${status2.manager}
2134
- `);
2135
- process.stdout.write(
2136
- `unit: ${status2.unitPath}${status2.installed ? "" : " (not written)"}
2137
- `
2138
- );
2139
- process.stdout.write(`installed: ${status2.installed ? "yes" : "no"}
2140
- `);
2141
- process.stdout.write(`running: ${runningLine(status2.running)}
2142
- `);
2143
- if (status2.manager === "systemd-user") {
2144
- process.stdout.write(`linger: ${status2.linger}${lingerNote(status2)}
2145
- `);
2146
- }
2147
- if (status2.installed) {
2148
- process.stdout.write("disable: cabane-companion service disable\n");
2149
- } else {
2150
- process.stdout.write("install: cabane-companion start (installs it for you)\n");
2151
- }
2152
- }
2153
- function serviceDisableCommand(deps = {}) {
2154
- const disable = deps.disable ?? disableService;
2155
- const res = disable();
2156
- if (!res.handled) {
2157
- process.stdout.write("service: nothing installed (nothing to remove).\n");
2158
- return;
2159
- }
2160
- if (!res.ok) {
2161
- process.stdout.write(
2162
- `service: nothing was removed \u2014 ${res.detail ?? "the service manager reported an error"}.
2163
- The unit is still installed and may still be running; \`cabane-companion service status\` shows where it stands.
2164
- `
2165
- );
2166
- process.exitCode = 1;
2167
- return;
2168
- }
2169
- process.stdout.write(
2170
- "service: stopped and removed. The companion no longer starts at login; `cabane-companion start` sets it up again.\n"
2171
- );
2172
- }
2173
- function runningLine(running) {
2174
- if (running === "transitional") {
2175
- return "starting or stopping \u2014 the manager reported a state in transition; ask again in a moment";
2176
- }
2177
- if (running === "unknown") return "unknown \u2014 the service manager didn't answer";
2178
- return running ? "yes" : "no";
2179
- }
2180
- function lingerNote(status2) {
2181
- if (status2.linger === "yes") return " (survives logout)";
2182
- if (status2.remoteSession) {
2183
- return " \u2014 without lingering a --user service stops when this SSH session ends";
2184
- }
2185
- return " \u2014 the service runs while you are logged in";
2186
- }
2187
-
2188
- // src/commands/start.ts
2189
- import { hostname as hostname2 } from "os";
2190
-
2191
1739
  // src/runtime.ts
2192
1740
  import { randomUUID as randomUUID2 } from "crypto";
2193
1741
 
2194
1742
  // src/dashboard/server.ts
2195
- import { dirname as dirname6, join as join9 } from "path";
2196
- import { fileURLToPath as fileURLToPath2 } from "url";
1743
+ import { dirname as dirname4, join as join7 } from "path";
1744
+ import { fileURLToPath } from "url";
2197
1745
  import { serve } from "@hono/node-server";
2198
1746
  import { Hono } from "hono";
2199
1747
 
2200
1748
  // src/dashboard/routes.ts
2201
- import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync7 } from "fs";
1749
+ import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync5 } from "fs";
2202
1750
  import { readFile } from "fs/promises";
2203
- import { extname, join as join8, normalize } from "path";
1751
+ import { extname, join as join6, normalize } from "path";
2204
1752
  import { streamSSE } from "hono/streaming";
2205
1753
 
2206
1754
  // src/state.ts
@@ -2449,14 +1997,14 @@ var CONTENT_TYPES = {
2449
1997
  function registerRoutes(app, deps) {
2450
1998
  const { supervisor, hub, staticDir } = deps;
2451
1999
  app.get("/", async (c) => {
2452
- const html = await readFile(join8(staticDir, "index.html"), "utf8");
2000
+ const html = await readFile(join6(staticDir, "index.html"), "utf8");
2453
2001
  return c.html(html);
2454
2002
  });
2455
2003
  app.get("/static/:file", async (c) => {
2456
2004
  const file = c.req.param("file");
2457
2005
  const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
2458
- const full = join8(staticDir, safe3);
2459
- if (!full.startsWith(staticDir) || !existsSync7(full)) return c.notFound();
2006
+ const full = join6(staticDir, safe3);
2007
+ if (!full.startsWith(staticDir) || !existsSync5(full)) return c.notFound();
2460
2008
  const body = await readFile(full);
2461
2009
  const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
2462
2010
  c.header("content-type", type);
@@ -2583,7 +2131,7 @@ function clampLimit(raw, fallback, max = 200) {
2583
2131
  return Math.min(Math.floor(n), max);
2584
2132
  }
2585
2133
  function tailFile(path, lines) {
2586
- if (!existsSync7(path)) return [];
2134
+ if (!existsSync5(path)) return [];
2587
2135
  const MAX_BYTES = 256 * 1024;
2588
2136
  let fd;
2589
2137
  try {
@@ -2672,7 +2220,7 @@ function isAddrInUse(err) {
2672
2220
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
2673
2221
  }
2674
2222
  function resolveStaticDir() {
2675
- return join9(dirname6(fileURLToPath2(import.meta.url)), "static");
2223
+ return join7(dirname4(fileURLToPath(import.meta.url)), "static");
2676
2224
  }
2677
2225
 
2678
2226
  // src/api.ts
@@ -3148,21 +2696,21 @@ function errorMessage2(status2, body) {
3148
2696
  }
3149
2697
 
3150
2698
  // src/cursor.ts
3151
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
3152
- import { join as join10 } from "path";
2699
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
2700
+ import { join as join8 } from "path";
3153
2701
  function pathFor(workspaceId) {
3154
- return join10(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
2702
+ return join8(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
3155
2703
  }
3156
2704
  function readCursor(workspaceId) {
3157
2705
  const path = pathFor(workspaceId);
3158
- if (!existsSync8(path)) return null;
3159
- const raw = readFileSync5(path, "utf8").trim();
2706
+ if (!existsSync6(path)) return null;
2707
+ const raw = readFileSync4(path, "utf8").trim();
3160
2708
  return raw.length > 0 ? raw : null;
3161
2709
  }
3162
2710
  function writeCursor(workspaceId, eventId) {
3163
2711
  const path = pathFor(workspaceId);
3164
- mkdirSync7(join10(cabaneDir(), "cursors"), { recursive: true });
3165
- writeFileSync5(path, eventId + "\n", "utf8");
2712
+ mkdirSync6(join8(cabaneDir(), "cursors"), { recursive: true });
2713
+ writeFileSync4(path, eventId + "\n", "utf8");
3166
2714
  }
3167
2715
 
3168
2716
  // src/cursor-tracker.ts
@@ -3205,20 +2753,20 @@ var CursorTracker = class {
3205
2753
  };
3206
2754
 
3207
2755
  // src/dispatch-dedupe.ts
3208
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
3209
- import { join as join11 } from "path";
2756
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
2757
+ import { join as join9 } from "path";
3210
2758
  var MAX_IDS = 256;
3211
2759
  function dir(log) {
3212
- return join11(cabaneDir(), log);
2760
+ return join9(cabaneDir(), log);
3213
2761
  }
3214
2762
  function pathFor2(log, workspaceId) {
3215
- return join11(dir(log), encodeURIComponent(workspaceId));
2763
+ return join9(dir(log), encodeURIComponent(workspaceId));
3216
2764
  }
3217
2765
  function readIds(log, workspaceId) {
3218
2766
  const path = pathFor2(log, workspaceId);
3219
- if (!existsSync9(path)) return [];
2767
+ if (!existsSync7(path)) return [];
3220
2768
  try {
3221
- return readFileSync6(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2769
+ return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
3222
2770
  } catch {
3223
2771
  return [];
3224
2772
  }
@@ -3231,8 +2779,8 @@ function mark(log, workspaceId, eventId) {
3231
2779
  if (ids.includes(eventId)) return;
3232
2780
  ids.push(eventId);
3233
2781
  const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
3234
- mkdirSync8(dir(log), { recursive: true });
3235
- writeFileSync6(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
2782
+ mkdirSync7(dir(log), { recursive: true });
2783
+ writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
3236
2784
  }
3237
2785
  function hasDispatched(workspaceId, eventId) {
3238
2786
  return has("dispatched", workspaceId, eventId);
@@ -3248,17 +2796,17 @@ function markCompleted(workspaceId, eventId) {
3248
2796
  }
3249
2797
  var MAX_RESUME_ATTEMPTS = 3;
3250
2798
  function resumeDir() {
3251
- return join11(cabaneDir(), "resume-attempts");
2799
+ return join9(cabaneDir(), "resume-attempts");
3252
2800
  }
3253
2801
  function resumePathFor(workspaceId) {
3254
- return join11(resumeDir(), encodeURIComponent(workspaceId));
2802
+ return join9(resumeDir(), encodeURIComponent(workspaceId));
3255
2803
  }
3256
2804
  function readResumeCounts(workspaceId) {
3257
2805
  const out = /* @__PURE__ */ new Map();
3258
2806
  const path = resumePathFor(workspaceId);
3259
- if (!existsSync9(path)) return out;
2807
+ if (!existsSync7(path)) return out;
3260
2808
  try {
3261
- for (const line of readFileSync6(path, "utf8").split("\n")) {
2809
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
3262
2810
  const trimmed = line.trim();
3263
2811
  if (!trimmed) continue;
3264
2812
  const tab = trimmed.lastIndexOf(" ");
@@ -3278,8 +2826,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
3278
2826
  counts.set(eventId, next);
3279
2827
  const entries = [...counts.entries()];
3280
2828
  const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
3281
- mkdirSync8(resumeDir(), { recursive: true });
3282
- writeFileSync6(
2829
+ mkdirSync7(resumeDir(), { recursive: true });
2830
+ writeFileSync5(
3283
2831
  resumePathFor(workspaceId),
3284
2832
  trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
3285
2833
  "utf8"
@@ -3451,13 +2999,55 @@ var turnEventSchema = z5.discriminatedUnion("type", [
3451
2999
  })
3452
3000
  ]);
3453
3001
 
3454
- // packages/agent-runtime/src/failure.ts
3002
+ // packages/agent-runtime/src/turn-diagnostics.ts
3455
3003
  import { z as z6 } from "zod";
3456
- var turnFailureSchema = z6.discriminatedUnion("kind", [
3004
+ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
3457
3005
  z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
3458
3006
  z6.object({ kind: z6.literal("rate_limited") }),
3459
3007
  z6.object({ kind: z6.literal("server_error") }),
3460
- z6.object({ kind: z6.literal("auth_expired") })
3008
+ z6.object({ kind: z6.literal("auth_expired") }),
3009
+ z6.object({ kind: z6.literal("no_result") }),
3010
+ z6.object({ kind: z6.literal("empty_result") }),
3011
+ z6.object({ kind: z6.literal("empty_result_unverified") }),
3012
+ z6.object({ kind: z6.literal("timeout_idle") }),
3013
+ z6.object({ kind: z6.literal("timeout_total") }),
3014
+ z6.object({ kind: z6.literal("cancelled") }),
3015
+ z6.object({ kind: z6.literal("skipped") }),
3016
+ z6.object({ kind: z6.literal("runtime_error") })
3017
+ ]);
3018
+ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
3019
+ var turnSessionModes = ["fresh", "resumed", "degraded"];
3020
+ var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
3021
+ var turnFinalSources = [
3022
+ "runtime_text",
3023
+ "progress_promotion",
3024
+ "host_fallback",
3025
+ "marker",
3026
+ "none"
3027
+ ];
3028
+ var turnDiagnosticsSchema = z6.object({
3029
+ outcome: z6.enum(turnOutcomes),
3030
+ resultReason: turnResultReasonSchema.nullable(),
3031
+ sessionMode: z6.enum(turnSessionModes),
3032
+ sessionFingerprint: z6.string().regex(/^[a-f0-9]{16}$/).nullable(),
3033
+ eventCounts: z6.object({
3034
+ session: z6.number().int().nonnegative(),
3035
+ text: z6.number().int().nonnegative(),
3036
+ thinking: z6.number().int().nonnegative(),
3037
+ tool: z6.number().int().nonnegative(),
3038
+ result: z6.number().int().nonnegative()
3039
+ }),
3040
+ runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
3041
+ finalSource: z6.enum(turnFinalSources)
3042
+ });
3043
+
3044
+ // packages/agent-runtime/src/failure.ts
3045
+ import { z as z7 } from "zod";
3046
+ var turnFailureSchema = z7.discriminatedUnion("kind", [
3047
+ z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
3048
+ z7.object({ kind: z7.literal("rate_limited") }),
3049
+ z7.object({ kind: z7.literal("server_error") }),
3050
+ z7.object({ kind: z7.literal("auth_expired") })
3461
3051
  ]);
3462
3052
  var USAGE_CAPPED = "usage_capped";
3463
3053
  var RATE_LIMITED = "rate_limited";
@@ -3562,64 +3152,89 @@ var NEGATED_CAP = /not (your|a) usage limit/g;
3562
3152
  var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
3563
3153
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
3564
3154
 
3155
+ // packages/agent-runtime/src/turn-diagnostics-normalize.ts
3156
+ function normalizeTurnResultReason(reason) {
3157
+ const classified = decodeFailureReason(reason);
3158
+ if (classified) return classified;
3159
+ switch (reason) {
3160
+ case "no_result":
3161
+ case "no_terminal":
3162
+ return { kind: "no_result" };
3163
+ case "empty_result":
3164
+ return { kind: "empty_result" };
3165
+ case "empty_result_unverified":
3166
+ return { kind: "empty_result_unverified" };
3167
+ case "timeout_idle":
3168
+ return { kind: "timeout_idle" };
3169
+ case "timeout_total":
3170
+ return { kind: "timeout_total" };
3171
+ case "cancelled":
3172
+ return { kind: "cancelled" };
3173
+ case "skipped":
3174
+ return { kind: "skipped" };
3175
+ default:
3176
+ return { kind: "runtime_error" };
3177
+ }
3178
+ }
3179
+
3565
3180
  // packages/agent-runtime/src/turn-request.ts
3566
- import { z as z7 } from "zod";
3567
- var contentBlockSchema = z7.discriminatedUnion("type", [
3568
- z7.object({ type: z7.literal("text"), text: z7.string() }),
3569
- z7.object({
3570
- type: z7.literal("image"),
3571
- source: z7.object({ type: z7.literal("url"), url: z7.string() })
3181
+ import { z as z8 } from "zod";
3182
+ var contentBlockSchema = z8.discriminatedUnion("type", [
3183
+ z8.object({ type: z8.literal("text"), text: z8.string() }),
3184
+ z8.object({
3185
+ type: z8.literal("image"),
3186
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
3572
3187
  }),
3573
- z7.object({
3574
- type: z7.literal("document"),
3575
- source: z7.object({ type: z7.literal("url"), url: z7.string() })
3188
+ z8.object({
3189
+ type: z8.literal("document"),
3190
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
3576
3191
  })
3577
3192
  ]);
3578
- var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
3579
- var resolvedRunConfigSchema = z7.object({
3580
- model: z7.string().nullable(),
3193
+ var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
3194
+ var resolvedRunConfigSchema = z8.object({
3195
+ model: z8.string().nullable(),
3581
3196
  effort: effortLevelSchema.optional(),
3582
- runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
3197
+ runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
3583
3198
  });
3584
- var resolvedMcpServerSchema = z7.union([
3585
- z7.object({
3586
- type: z7.literal("stdio").optional(),
3587
- command: z7.string(),
3588
- args: z7.array(z7.string()).optional(),
3589
- env: z7.record(z7.string(), z7.string()).optional()
3199
+ var resolvedMcpServerSchema = z8.union([
3200
+ z8.object({
3201
+ type: z8.literal("stdio").optional(),
3202
+ command: z8.string(),
3203
+ args: z8.array(z8.string()).optional(),
3204
+ env: z8.record(z8.string(), z8.string()).optional()
3590
3205
  }),
3591
- z7.object({
3592
- type: z7.literal("http"),
3593
- url: z7.string(),
3594
- headers: z7.record(z7.string(), z7.string()).optional()
3206
+ z8.object({
3207
+ type: z8.literal("http"),
3208
+ url: z8.string(),
3209
+ headers: z8.record(z8.string(), z8.string()).optional()
3595
3210
  }),
3596
- z7.object({
3597
- type: z7.literal("sse"),
3598
- url: z7.string(),
3599
- headers: z7.record(z7.string(), z7.string()).optional()
3211
+ z8.object({
3212
+ type: z8.literal("sse"),
3213
+ url: z8.string(),
3214
+ headers: z8.record(z8.string(), z8.string()).optional()
3600
3215
  })
3601
3216
  ]);
3602
- var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
3603
- var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
3604
- var turnRequestSchema = z7.object({
3217
+ var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
3218
+ var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
3219
+ var turnRequestSchema = z8.object({
3605
3220
  // Server-composed system prompt (core + capability prose + adapter addendum +
3606
3221
  // charter). One string to the adapter.
3607
- systemPrompt: z7.string(),
3222
+ systemPrompt: z8.string(),
3608
3223
  // Server-composed per-turn user text (anchor reminder + the triggering message).
3609
- prompt: z7.string(),
3224
+ prompt: z8.string(),
3610
3225
  // The multi-block user-message body (text + vision).
3611
- content: z7.array(contentBlockSchema),
3226
+ content: z8.array(contentBlockSchema),
3612
3227
  // Portable-or-dialect run-config (above).
3613
3228
  config: resolvedRunConfigSchema,
3614
3229
  // Abstract capability grants; the adapter maps them to tool names.
3615
3230
  policy: hostPolicySchema,
3616
3231
  // Prior opaque session state, or null for a fresh session.
3617
- session: z7.string().nullable(),
3232
+ session: z8.string().nullable(),
3618
3233
  // The cabane control-plane coordinates for this turn's MCP + post-back.
3619
- cabane: z7.object({
3620
- mcpUrl: z7.string(),
3621
- bearer: z7.string(),
3622
- activeConversationId: z7.string(),
3234
+ cabane: z8.object({
3235
+ mcpUrl: z8.string(),
3236
+ bearer: z8.string(),
3237
+ activeConversationId: z8.string(),
3623
3238
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
3624
3239
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
3625
3240
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -3629,7 +3244,7 @@ var turnRequestSchema = z7.object({
3629
3244
  // claude-code ignores it (it mounts the in-process instance instead), and
3630
3245
  // every existing `cabane`-block fixture keeps parsing unchanged; the
3631
3246
  // companion always populates it (`build-options.ts`).
3632
- turnControlUrl: z7.string().optional(),
3247
+ turnControlUrl: z8.string().optional(),
3633
3248
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
3634
3249
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
3635
3250
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -3639,39 +3254,39 @@ var turnRequestSchema = z7.object({
3639
3254
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
3640
3255
  // always populates it (`build-options.ts`), and the native adapter fails the
3641
3256
  // turn loudly when it is somehow absent rather than guessing.
3642
- workspaceId: z7.string().optional(),
3257
+ workspaceId: z8.string().optional(),
3643
3258
  // CT752: the server-resolved workspace surface this credential exposes.
3644
3259
  // Readiness uses this explicit fact to require `sdk` for code mode and the
3645
3260
  // granular floor for classic mode; inventory contents alone cannot infer it
3646
3261
  // because `sdk` is intentionally also available on the classic surface.
3647
- workspaceToolSurface: z7.enum(["code", "classic"]).optional()
3262
+ workspaceToolSurface: z8.enum(["code", "classic"]).optional()
3648
3263
  }),
3649
3264
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
3650
3265
  // prepare hook, and the resolved user MCP servers.
3651
- local: z7.object({
3652
- cwd: z7.string().optional(),
3653
- env: z7.record(z7.string(), z7.string()).optional(),
3266
+ local: z8.object({
3267
+ cwd: z8.string().optional(),
3268
+ env: z8.record(z8.string(), z8.string()).optional(),
3654
3269
  mcpServers: resolvedMcpServersSchema.optional(),
3655
3270
  // CT289: machine-local claude-code adapter knobs the operator sets on a
3656
3271
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
3657
3272
  // true` opts back into Claude Code's auto-memory (governed by the operator's
3658
3273
  // own `.claude/settings.json`); absent/false leaves the adapter's force-off
3659
3274
  // default in place (see `buildClaudeCodeOptions`).
3660
- claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
3275
+ claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
3661
3276
  }),
3662
3277
  // Host-owned injected servers (host-filled) — e.g. the summon server.
3663
- extra: z7.object({
3278
+ extra: z8.object({
3664
3279
  mcpServers: hostInjectedServersSchema
3665
3280
  })
3666
3281
  });
3667
3282
 
3668
3283
  // packages/agent-runtime/src/conformance.ts
3669
- import { z as z8 } from "zod";
3670
- var conformanceFixtureSchema = z8.object({
3671
- name: z8.string(),
3284
+ import { z as z9 } from "zod";
3285
+ var conformanceFixtureSchema = z9.object({
3286
+ name: z9.string(),
3672
3287
  request: turnRequestSchema,
3673
- nativeStream: z8.array(z8.unknown()),
3674
- expected: z8.array(turnEventSchema)
3288
+ nativeStream: z9.array(z9.unknown()),
3289
+ expected: z9.array(turnEventSchema)
3675
3290
  });
3676
3291
 
3677
3292
  // packages/agent-runtime/src/transcript.ts
@@ -3891,6 +3506,7 @@ var TurnPump = class {
3891
3506
  if (evt.final) {
3892
3507
  this.emittedFinal = true;
3893
3508
  this.finalReplyBody = body;
3509
+ this.emittedFinalSource = "runtime_text";
3894
3510
  } else {
3895
3511
  this.lastProgressBody = body;
3896
3512
  }
@@ -3929,6 +3545,7 @@ var TurnPump = class {
3929
3545
  lastProgressBody = null;
3930
3546
  // The turn's final reply text, captured for the dashboard feed / logging.
3931
3547
  finalReplyBody = null;
3548
+ emittedFinalSource = "none";
3932
3549
  // CT11: each tool's persisted seq so the `done`/`error` frame republishes the
3933
3550
  // same seq the `start` row got (the host's upsert keeps the original seq).
3934
3551
  toolSeqByUseId = /* @__PURE__ */ new Map();
@@ -3951,6 +3568,7 @@ var TurnPump = class {
3951
3568
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
3952
3569
  this.emittedFinal = true;
3953
3570
  this.finalReplyBody = body;
3571
+ this.emittedFinalSource = this.lastProgressBody ? "progress_promotion" : "host_fallback";
3954
3572
  } catch (err) {
3955
3573
  this.opts.onError?.(err, "empty-final");
3956
3574
  }
@@ -3965,6 +3583,9 @@ var TurnPump = class {
3965
3583
  get replyBody() {
3966
3584
  return this.finalReplyBody;
3967
3585
  }
3586
+ get finalSource() {
3587
+ return this.emittedFinalSource;
3588
+ }
3968
3589
  };
3969
3590
 
3970
3591
  // packages/agent-runtime/src/claude-code/index.ts
@@ -3986,7 +3607,7 @@ var CLAUDE_CODE_ADDENDUM = [
3986
3607
  ].join(" ");
3987
3608
 
3988
3609
  // packages/agent-runtime/src/claude-code/policy.ts
3989
- import { z as z9 } from "zod";
3610
+ import { z as z10 } from "zod";
3990
3611
  var HOST_FS_TOOLS = [
3991
3612
  // shell + local filesystem
3992
3613
  "Bash",
@@ -4036,18 +3657,18 @@ function withThinkingSummaries(thinking) {
4036
3657
  if (thinking.type === "disabled") return thinking;
4037
3658
  return { display: "summarized", ...thinking };
4038
3659
  }
4039
- var claudeCodeDialectSchema = z9.object({
4040
- thinking: z9.discriminatedUnion("type", [
4041
- z9.object({
4042
- type: z9.literal("adaptive"),
4043
- display: z9.enum(["summarized", "omitted"]).optional()
3660
+ var claudeCodeDialectSchema = z10.object({
3661
+ thinking: z10.discriminatedUnion("type", [
3662
+ z10.object({
3663
+ type: z10.literal("adaptive"),
3664
+ display: z10.enum(["summarized", "omitted"]).optional()
4044
3665
  }),
4045
- z9.object({
4046
- type: z9.literal("enabled"),
4047
- budgetTokens: z9.number().int().positive().optional(),
4048
- display: z9.enum(["summarized", "omitted"]).optional()
3666
+ z10.object({
3667
+ type: z10.literal("enabled"),
3668
+ budgetTokens: z10.number().int().positive().optional(),
3669
+ display: z10.enum(["summarized", "omitted"]).optional()
4049
3670
  }),
4050
- z9.object({ type: z9.literal("disabled") })
3671
+ z10.object({ type: z10.literal("disabled") })
4051
3672
  ]).optional()
4052
3673
  }).loose();
4053
3674
  function readThinking(runtimeOptions) {
@@ -4989,7 +4610,7 @@ function sealHeld(held, terminal) {
4989
4610
  }
4990
4611
 
4991
4612
  // packages/agent-runtime/src/opencode/policy.ts
4992
- import { z as z10 } from "zod";
4613
+ import { z as z11 } from "zod";
4993
4614
  var OPENCODE_HOST_TOOLS = [
4994
4615
  "bash",
4995
4616
  "edit",
@@ -5016,8 +4637,8 @@ function opencodeToolPolicy(policy) {
5016
4637
  deny(OPENCODE_UI_PROMPT_TOOLS);
5017
4638
  return { tools, allowAllHostTools: policy.hostFs };
5018
4639
  }
5019
- var opencodeDialectSchema = z10.object({
5020
- agent: z10.string().min(1).optional()
4640
+ var opencodeDialectSchema = z11.object({
4641
+ agent: z11.string().min(1).optional()
5021
4642
  }).loose();
5022
4643
  function readOpencodeDialect(runtimeOptions) {
5023
4644
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -6027,7 +5648,7 @@ function sealHeld2(held, terminal) {
6027
5648
  }
6028
5649
 
6029
5650
  // packages/agent-runtime/src/codex/policy.ts
6030
- import { z as z11 } from "zod";
5651
+ import { z as z12 } from "zod";
6031
5652
  function codexToolPolicy(policy) {
6032
5653
  return policy.hostFs ? {
6033
5654
  sandboxMode: "danger-full-access",
@@ -6046,8 +5667,8 @@ function codexToolPolicy(policy) {
6046
5667
  };
6047
5668
  }
6048
5669
  var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
6049
- var codexDialectSchema = z11.object({
6050
- modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
5670
+ var codexDialectSchema = z12.object({
5671
+ modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
6051
5672
  }).loose();
6052
5673
  function readCodexDialect(runtimeOptions) {
6053
5674
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -6976,12 +6597,12 @@ var ConnectorHealthStore = class {
6976
6597
  };
6977
6598
 
6978
6599
  // src/dispatcher.ts
6979
- import { randomUUID } from "crypto";
6980
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readdirSync as readdirSync2, statSync } from "fs";
6981
- import { join as join16 } from "path";
6600
+ import { createHash as createHash2, randomUUID } from "crypto";
6601
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
6602
+ import { join as join14 } from "path";
6982
6603
 
6983
6604
  // src/summon.ts
6984
- import { z as z12 } from "zod";
6605
+ import { z as z13 } from "zod";
6985
6606
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
6986
6607
  var SUMMON_AGENT_TOOL = "summon_agent";
6987
6608
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -7029,7 +6650,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7029
6650
  SUMMON_AGENT_TOOL,
7030
6651
  "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` \u2014 every agent's handle and id is on the roster in your turn context. The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `cabane.conversations.create` / `cabane.conversations.post` with their `dispatch` field instead.",
7031
6652
  {
7032
- agentId: z12.string().uuid().describe(
6653
+ agentId: z13.string().uuid().describe(
7033
6654
  "The peer agent to summon \u2014 a workspace agent id, from your turn context's roster."
7034
6655
  )
7035
6656
  },
@@ -7046,7 +6667,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7046
6667
  SKIP_TURN_TOOL,
7047
6668
  `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
7048
6669
  {
7049
- reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6670
+ reason: z13.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
7050
6671
  },
7051
6672
  async (args) => {
7052
6673
  skipState.skipped = true;
@@ -7063,23 +6684,23 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7063
6684
  ASK_TOOL,
7064
6685
  "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
7065
6686
  {
7066
- targetUserId: z12.string().uuid().describe(
6687
+ targetUserId: z13.string().uuid().describe(
7067
6688
  "The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
7068
6689
  ),
7069
- question: z12.string().min(1).max(400).optional().describe(
6690
+ question: z13.string().min(1).max(400).optional().describe(
7070
6691
  "SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
7071
6692
  ),
7072
- headline: z12.string().min(1).max(120).optional().describe(
6693
+ headline: z13.string().min(1).max(120).optional().describe(
7073
6694
  'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
7074
6695
  ),
7075
- options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
7076
- questions: z12.array(
7077
- z12.object({
7078
- headline: z12.string().min(1).max(120).describe(
6696
+ options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
6697
+ questions: z13.array(
6698
+ z13.object({
6699
+ headline: z13.string().min(1).max(120).describe(
7079
6700
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
7080
6701
  ),
7081
- body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
7082
- options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
6702
+ body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
6703
+ options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
7083
6704
  })
7084
6705
  ).min(1).max(5).optional().describe(
7085
6706
  "MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
@@ -7136,13 +6757,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7136
6757
  SUB_AGENT_TOOL,
7137
6758
  "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
7138
6759
  {
7139
- prompt: z12.string().min(1).max(65536).describe(
6760
+ prompt: z13.string().min(1).max(65536).describe(
7140
6761
  "The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
7141
6762
  ),
7142
- agentId: z12.string().uuid().optional().describe(
6763
+ agentId: z13.string().uuid().optional().describe(
7143
6764
  "Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
7144
6765
  ),
7145
- title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6766
+ title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7146
6767
  },
7147
6768
  async (args) => {
7148
6769
  const result = await subAgentCreate(args);
@@ -7172,13 +6793,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7172
6793
  WAKE_ME_TOOL,
7173
6794
  "Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
7174
6795
  {
7175
- afterSeconds: z12.number().int().positive().optional().describe(
6796
+ afterSeconds: z13.number().int().positive().optional().describe(
7176
6797
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
7177
6798
  ),
7178
- at: z12.string().datetime({ offset: true }).optional().describe(
6799
+ at: z13.string().datetime({ offset: true }).optional().describe(
7179
6800
  "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
7180
6801
  ),
7181
- note: z12.string().min(1).max(2e3).describe(
6802
+ note: z13.string().min(1).max(2e3).describe(
7182
6803
  'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
7183
6804
  )
7184
6805
  },
@@ -7311,11 +6932,11 @@ function trimSlash2(s) {
7311
6932
  // src/codex-instructions.ts
7312
6933
  import { mkdtemp, rm, writeFile } from "fs/promises";
7313
6934
  import { tmpdir } from "os";
7314
- import { join as join12 } from "path";
6935
+ import { join as join10 } from "path";
7315
6936
  var PREFIX = "cabane-codex-instructions-";
7316
6937
  async function writeCodexInstructionsFile(contents) {
7317
- const dir2 = await mkdtemp(join12(tmpdir(), PREFIX));
7318
- const path = join12(dir2, "instructions.md");
6938
+ const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
6939
+ const path = join10(dir2, "instructions.md");
7319
6940
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
7320
6941
  return {
7321
6942
  path,
@@ -7326,22 +6947,22 @@ async function writeCodexInstructionsFile(contents) {
7326
6947
  }
7327
6948
 
7328
6949
  // src/prepared.ts
7329
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync7, existsSync as existsSync10 } from "fs";
7330
- import { join as join13 } from "path";
6950
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
6951
+ import { join as join11 } from "path";
7331
6952
  function dirFor(workspaceId) {
7332
- return join13(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6953
+ return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
7333
6954
  }
7334
6955
  function conversationDir(workspaceId, conversationId) {
7335
- return join13(dirFor(workspaceId), encodeURIComponent(conversationId));
6956
+ return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
7336
6957
  }
7337
6958
  function pathFor3(workspaceId, conversationId, agentId) {
7338
- return join13(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6959
+ return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
7339
6960
  }
7340
6961
  function readPrepared(workspaceId, conversationId, agentId) {
7341
6962
  const path = pathFor3(workspaceId, conversationId, agentId);
7342
- if (!existsSync10(path)) return null;
6963
+ if (!existsSync8(path)) return null;
7343
6964
  try {
7344
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
6965
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
7345
6966
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
7346
6967
  return {
7347
6968
  cwd: parsed.cwd,
@@ -7354,32 +6975,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
7354
6975
  }
7355
6976
  }
7356
6977
  function writePrepared(workspaceId, conversationId, agentId, result) {
7357
- mkdirSync9(conversationDir(workspaceId, conversationId), { recursive: true });
7358
- writeFileSync7(
6978
+ mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
6979
+ writeFileSync6(
7359
6980
  pathFor3(workspaceId, conversationId, agentId),
7360
6981
  JSON.stringify(result) + "\n",
7361
6982
  "utf8"
7362
6983
  );
7363
6984
  }
7364
6985
  function clearPrepared(workspaceId, conversationId, agentId) {
7365
- rmSync6(pathFor3(workspaceId, conversationId, agentId), { force: true });
6986
+ rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
7366
6987
  }
7367
6988
 
7368
6989
  // src/secrets.ts
7369
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
7370
- import { join as join14 } from "path";
7371
- import { z as z13 } from "zod";
6990
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
6991
+ import { join as join12 } from "path";
6992
+ import { z as z14 } from "zod";
7372
6993
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7373
6994
  function secretsPath() {
7374
- return join14(cabaneDir(), "secrets.json");
6995
+ return join12(cabaneDir(), "secrets.json");
7375
6996
  }
7376
- var secretStoreSchema = z13.record(z13.string(), z13.string());
6997
+ var secretStoreSchema = z14.record(z14.string(), z14.string());
7377
6998
  function loadSecretStore() {
7378
6999
  const path = secretsPath();
7379
- if (!existsSync11(path)) return makeStore({});
7000
+ if (!existsSync9(path)) return makeStore({});
7380
7001
  let raw;
7381
7002
  try {
7382
- raw = readFileSync8(path, "utf8");
7003
+ raw = readFileSync7(path, "utf8");
7383
7004
  } catch (err) {
7384
7005
  throw new ConfigError(
7385
7006
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -7457,21 +7078,22 @@ function resolveMcpSecrets(mcpServers, store) {
7457
7078
  }
7458
7079
 
7459
7080
  // src/transcript-writer.ts
7460
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync10, readdirSync, rmSync as rmSync7 } from "fs";
7461
- import { join as join15 } from "path";
7081
+ import { appendFileSync, chmodSync as chmodSync3, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
7082
+ import { basename, dirname as dirname5, join as join13 } from "path";
7462
7083
  function transcriptsDir() {
7463
- return join15(cabaneDir(), "transcripts");
7084
+ return join13(cabaneDir(), "transcripts");
7464
7085
  }
7465
7086
  var RETAIN = 200;
7087
+ var ANOMALY_RETAIN = 50;
7466
7088
  var TranscriptWriter = class {
7467
7089
  path;
7468
7090
  broken = false;
7469
7091
  onWarn;
7470
7092
  constructor(dir2, meta, onWarn) {
7471
7093
  this.onWarn = onWarn;
7472
- this.path = join15(dir2, fileName(meta));
7094
+ this.path = join13(dir2, fileName(meta));
7473
7095
  try {
7474
- mkdirSync10(dir2, { recursive: true });
7096
+ mkdirSync9(dir2, { recursive: true });
7475
7097
  try {
7476
7098
  chmodSync3(dir2, 448);
7477
7099
  } catch {
@@ -7490,6 +7112,25 @@ var TranscriptWriter = class {
7490
7112
  close(outcome) {
7491
7113
  this.line({ type: "_outcome", ...outcome });
7492
7114
  }
7115
+ // CT1145: keep an independent copy of an empty successful envelope after the
7116
+ // footer has landed. The ordinary 200-file FIFO keeps rotating as before;
7117
+ // this bucket retains the newest 50 anomalies regardless of ordinary traffic.
7118
+ // Best-effort like every other transcript operation: observability can never
7119
+ // fail the turn.
7120
+ preserveAnomaly() {
7121
+ if (this.broken) return;
7122
+ try {
7123
+ const dir2 = join13(dirname5(this.path), "anomalies");
7124
+ mkdirSync9(dir2, { recursive: true, mode: 448 });
7125
+ chmodSync3(dir2, 448);
7126
+ const target = join13(dir2, basename(this.path));
7127
+ copyFileSync(this.path, target);
7128
+ chmodSync3(target, 384);
7129
+ pruneOld(dir2, ANOMALY_RETAIN);
7130
+ } catch (err) {
7131
+ this.fail(err);
7132
+ }
7133
+ }
7493
7134
  line(obj) {
7494
7135
  if (this.broken) return;
7495
7136
  try {
@@ -7528,7 +7169,7 @@ function pruneOld(dir2, retain) {
7528
7169
  const drop = files.sort().slice(0, files.length - retain);
7529
7170
  for (const f of drop) {
7530
7171
  try {
7531
- rmSync7(join15(dir2, f), { force: true });
7172
+ rmSync6(join13(dir2, f), { force: true });
7532
7173
  } catch {
7533
7174
  }
7534
7175
  }
@@ -7647,13 +7288,16 @@ var TurnCommitter = class {
7647
7288
  get replyBody() {
7648
7289
  return this.pump.replyBody;
7649
7290
  }
7291
+ get finalSource() {
7292
+ return this.pump.finalSource;
7293
+ }
7650
7294
  // CT183: resolve the in-thread summon into the `dispatch` field for a `final`
7651
7295
  // commit. A self-target is stripped here (mirror of the in-app self-strip); the
7652
7296
  // server strips it again and resolves / ignores an unknown id.
7653
7297
  summonField() {
7654
- const target2 = this.deps.summonState.agentId;
7655
- if (!target2 || target2 === this.deps.agentId) return {};
7656
- return { dispatch: target2 };
7298
+ const target = this.deps.summonState.agentId;
7299
+ if (!target || target === this.deps.agentId) return {};
7300
+ return { dispatch: target };
7657
7301
  }
7658
7302
  // CT326: resolve the per-turn ask into the `ask` field for a `final` commit.
7659
7303
  // The server validates the target (must be a workspace member/owner) and
@@ -7874,7 +7518,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7874
7518
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7875
7519
  function checkoutState(cwd) {
7876
7520
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
7877
- if (!existsSync12(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
7521
+ if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
7878
7522
  let entries;
7879
7523
  try {
7880
7524
  entries = readdirSync2(cwd);
@@ -7884,15 +7528,15 @@ function checkoutState(cwd) {
7884
7528
  if (entries.length === 0) {
7885
7529
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
7886
7530
  }
7887
- const gitPath = join16(cwd, ".git");
7888
- if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
7531
+ const gitPath = join14(cwd, ".git");
7532
+ if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
7889
7533
  let stat;
7890
7534
  try {
7891
7535
  stat = statSync(gitPath);
7892
7536
  } catch (error) {
7893
7537
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
7894
7538
  }
7895
- if (stat.isDirectory() && !existsSync12(join16(gitPath, "HEAD")))
7539
+ if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
7896
7540
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
7897
7541
  return { ok: true, reason: "usable" };
7898
7542
  }
@@ -8083,7 +7727,7 @@ var Dispatcher = class {
8083
7727
  let seqCounter = 0;
8084
7728
  const nextSeq = () => ++seqCounter;
8085
7729
  let effectiveCwd = localCwd ?? cabaneCwd;
8086
- if (effectiveCwd && !existsSync12(effectiveCwd)) {
7730
+ if (effectiveCwd && !existsSync10(effectiveCwd)) {
8087
7731
  turnLog.warn(
8088
7732
  { cwd: effectiveCwd },
8089
7733
  "dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
@@ -8347,11 +7991,11 @@ ${reason}`,
8347
7991
  const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
8348
7992
  const closeTurnReceipt = (ok, reason) => {
8349
7993
  if (!turnReceiptPath) return;
8350
- const target2 = turnReceiptPath;
7994
+ const target = turnReceiptPath;
8351
7995
  turnReceiptPath = null;
8352
7996
  try {
8353
7997
  appendFileSync2(
8354
- target2,
7998
+ target,
8355
7999
  `${JSON.stringify({
8356
8000
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8357
8001
  event: "settled",
@@ -8391,7 +8035,7 @@ ${reason}`,
8391
8035
  }
8392
8036
  return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
8393
8037
  }
8394
- const receiptPath = join16(effectiveCwd, ".git", "cabane", "readiness.jsonl");
8038
+ const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
8395
8039
  const receiptLine = (fields) => `${JSON.stringify({
8396
8040
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8397
8041
  taskId: hookEnv.CABANE_TASK_ID,
@@ -8415,7 +8059,7 @@ ${reason}`,
8415
8059
  })}
8416
8060
  `;
8417
8061
  try {
8418
- mkdirSync11(join16(effectiveCwd, ".git", "cabane"), { recursive: true });
8062
+ mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
8419
8063
  appendFileSync2(
8420
8064
  receiptPath,
8421
8065
  // `starting` is the honest classification before the proof has run. The
@@ -8510,6 +8154,16 @@ ${reason}`,
8510
8154
  let turnUsage;
8511
8155
  let turnResolvedModel;
8512
8156
  let turnResolvedConfig;
8157
+ const eventCounts = {
8158
+ session: 0,
8159
+ text: 0,
8160
+ thinking: 0,
8161
+ tool: 0,
8162
+ result: 0
8163
+ };
8164
+ let runtimeResultKind = null;
8165
+ let latestSessionState = request.session;
8166
+ let settledDiagnostics = null;
8513
8167
  const committer = new TurnCommitter({
8514
8168
  api: this.opts.api,
8515
8169
  workspaceId,
@@ -8605,6 +8259,7 @@ ${reason}`,
8605
8259
  try {
8606
8260
  for await (const event of adapter.runTurn(request, abortController.signal)) {
8607
8261
  transcript2?.write(event);
8262
+ eventCounts[event.type] += 1;
8608
8263
  armIdle();
8609
8264
  if (abortController.signal.aborted) {
8610
8265
  turnLog.info("dispatcher: aborted mid-turn");
@@ -8613,6 +8268,7 @@ ${reason}`,
8613
8268
  break;
8614
8269
  }
8615
8270
  if (event.type === "session") {
8271
+ latestSessionState = event.state;
8616
8272
  if (event.degraded) sessionDegraded = true;
8617
8273
  if (!sessionWritten) {
8618
8274
  sessionWritten = true;
@@ -8651,6 +8307,7 @@ ${reason}`,
8651
8307
  turnUsage = event.usage;
8652
8308
  turnResolvedModel = event.resolvedModel;
8653
8309
  turnResolvedConfig = event.resolvedConfig;
8310
+ runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
8654
8311
  } else if (event.type === "text" && skipState.skipped) {
8655
8312
  } else {
8656
8313
  if (event.type === "text" && event.terminal) {
@@ -8773,6 +8430,34 @@ ${reason}`,
8773
8430
  body.sessionWriteRejected = true;
8774
8431
  this.sessionWriteNotified.add(key);
8775
8432
  }
8433
+ const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
8434
+ const noContentEvents = eventCounts.text === 0 && eventCounts.thinking === 0 && eventCounts.tool === 0;
8435
+ const emptySuccessfulEnvelope = outcome === "success" && eventCounts.result > 0 && noContentEvents;
8436
+ const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : emptySuccessfulEnvelope ? turnUsage?.inputTokens === 0 && turnUsage.outputTokens === 0 ? { kind: "empty_result" } : { kind: "empty_result_unverified" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
8437
+ settledDiagnostics = {
8438
+ outcome,
8439
+ resultReason: diagnosticReason,
8440
+ sessionMode: sessionDegraded ? "degraded" : request.session ? "resumed" : "fresh",
8441
+ sessionFingerprint: fingerprintSessionState(latestSessionState),
8442
+ eventCounts,
8443
+ runtimeResultKind,
8444
+ finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
8445
+ };
8446
+ body.diagnostics = settledDiagnostics;
8447
+ if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
8448
+ diagnosticReason.kind
8449
+ )) {
8450
+ turnLog.warn(
8451
+ {
8452
+ turnId,
8453
+ conversationId: payload.conversationId,
8454
+ agentId: payload.agentId,
8455
+ runtime: turnRuntime,
8456
+ diagnostics: settledDiagnostics
8457
+ },
8458
+ "dispatcher: anomalous turn settled"
8459
+ );
8460
+ }
8776
8461
  this.opts.connectorHealth?.recordSettle(turnRuntime, {
8777
8462
  ok: okResult,
8778
8463
  errorReason: body.errorReason ?? null
@@ -8805,6 +8490,9 @@ ${reason}`,
8805
8490
  if (!okResult && resultReason !== "cancelled") {
8806
8491
  turnLog.info(`turn failed \u2014 full transcript: ${transcript2.path}`);
8807
8492
  }
8493
+ if (settledDiagnostics?.resultReason?.kind === "empty_result" || settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
8494
+ transcript2.preserveAnomaly();
8495
+ }
8808
8496
  }
8809
8497
  if (okResult) {
8810
8498
  turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
@@ -8842,6 +8530,17 @@ ${reason}`,
8842
8530
  return true;
8843
8531
  }
8844
8532
  };
8533
+ function fingerprintSessionState(state) {
8534
+ if (!state) return null;
8535
+ let opaqueId = state;
8536
+ try {
8537
+ const parsed = JSON.parse(state);
8538
+ const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
8539
+ if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
8540
+ } catch {
8541
+ }
8542
+ return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
8543
+ }
8845
8544
 
8846
8545
  // src/opencode-models.ts
8847
8546
  var OPENCODE_RUNTIME = "opencode";
@@ -8886,15 +8585,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
8886
8585
 
8887
8586
  // src/outbox.ts
8888
8587
  import {
8889
- existsSync as existsSync13,
8890
- mkdirSync as mkdirSync12,
8588
+ existsSync as existsSync11,
8589
+ mkdirSync as mkdirSync11,
8891
8590
  readdirSync as readdirSync3,
8892
- readFileSync as readFileSync9,
8591
+ readFileSync as readFileSync8,
8893
8592
  renameSync as renameSync3,
8894
- rmSync as rmSync8,
8895
- writeFileSync as writeFileSync8
8593
+ rmSync as rmSync7,
8594
+ writeFileSync as writeFileSync7
8896
8595
  } from "fs";
8897
- import { join as join17 } from "path";
8596
+ import { join as join15 } from "path";
8898
8597
  var MAX_ENTRIES = 2e3;
8899
8598
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
8900
8599
  var Outbox = class {
@@ -8907,25 +8606,25 @@ var Outbox = class {
8907
8606
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
8908
8607
  // cases route writes at the right tmpdir.
8909
8608
  dir() {
8910
- return join17(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
8609
+ return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
8911
8610
  }
8912
8611
  fileFor(turnId, seq) {
8913
- return join17(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
8612
+ return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
8914
8613
  }
8915
8614
  // Persist a commit for later draining. Atomic (temp file + rename) so a
8916
8615
  // concurrent `list()` never reads a half-written entry, then enforces the
8917
8616
  // per-workspace bounds.
8918
8617
  persist(entry) {
8919
8618
  const dir2 = this.dir();
8920
- mkdirSync12(dir2, { recursive: true });
8921
- const target2 = this.fileFor(entry.turnId, entry.seq);
8922
- const tmp = `${target2}.${process.pid}.tmp`;
8619
+ mkdirSync11(dir2, { recursive: true });
8620
+ const target = this.fileFor(entry.turnId, entry.seq);
8621
+ const tmp = `${target}.${process.pid}.tmp`;
8923
8622
  try {
8924
- writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
8925
- renameSync3(tmp, target2);
8623
+ writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
8624
+ renameSync3(tmp, target);
8926
8625
  } catch (err) {
8927
8626
  try {
8928
- rmSync8(tmp, { force: true });
8627
+ rmSync7(tmp, { force: true });
8929
8628
  } catch {
8930
8629
  }
8931
8630
  this.log?.warn(
@@ -8942,7 +8641,7 @@ var Outbox = class {
8942
8641
  // wedging the drain.
8943
8642
  list() {
8944
8643
  const dir2 = this.dir();
8945
- if (!existsSync13(dir2)) return [];
8644
+ if (!existsSync11(dir2)) return [];
8946
8645
  let names;
8947
8646
  try {
8948
8647
  names = readdirSync3(dir2);
@@ -8952,9 +8651,9 @@ var Outbox = class {
8952
8651
  const entries = [];
8953
8652
  for (const name of names) {
8954
8653
  if (!name.endsWith(".json")) continue;
8955
- const full = join17(dir2, name);
8654
+ const full = join15(dir2, name);
8956
8655
  try {
8957
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
8656
+ const parsed = JSON.parse(readFileSync8(full, "utf8"));
8958
8657
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
8959
8658
  entries.push(parsed);
8960
8659
  } else {
@@ -8972,13 +8671,13 @@ var Outbox = class {
8972
8671
  // Remove a delivered (or terminally-discarded) entry. No-op if already gone.
8973
8672
  remove(turnId, seq) {
8974
8673
  try {
8975
- rmSync8(this.fileFor(turnId, seq), { force: true });
8674
+ rmSync7(this.fileFor(turnId, seq), { force: true });
8976
8675
  } catch {
8977
8676
  }
8978
8677
  }
8979
8678
  size() {
8980
8679
  const dir2 = this.dir();
8981
- if (!existsSync13(dir2)) return 0;
8680
+ if (!existsSync11(dir2)) return 0;
8982
8681
  try {
8983
8682
  return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
8984
8683
  } catch {
@@ -8991,7 +8690,7 @@ var Outbox = class {
8991
8690
  "companion outbox: dropping unreadable entry"
8992
8691
  );
8993
8692
  try {
8994
- rmSync8(full, { force: true });
8693
+ rmSync7(full, { force: true });
8995
8694
  } catch {
8996
8695
  }
8997
8696
  }
@@ -9026,43 +8725,43 @@ var Outbox = class {
9026
8725
  };
9027
8726
 
9028
8727
  // src/run-config.ts
9029
- import { z as z14 } from "zod";
9030
- var mcpStdioServerSchema = z14.object({
9031
- type: z14.literal("stdio").optional(),
9032
- command: z14.string().min(1),
9033
- args: z14.array(z14.string()).optional(),
9034
- env: z14.record(z14.string(), z14.string()).optional()
8728
+ import { z as z15 } from "zod";
8729
+ var mcpStdioServerSchema = z15.object({
8730
+ type: z15.literal("stdio").optional(),
8731
+ command: z15.string().min(1),
8732
+ args: z15.array(z15.string()).optional(),
8733
+ env: z15.record(z15.string(), z15.string()).optional()
9035
8734
  });
9036
- var mcpHttpServerSchema = z14.object({
9037
- type: z14.literal("http"),
9038
- url: z14.string().url(),
9039
- headers: z14.record(z14.string(), z14.string()).optional()
8735
+ var mcpHttpServerSchema = z15.object({
8736
+ type: z15.literal("http"),
8737
+ url: z15.string().url(),
8738
+ headers: z15.record(z15.string(), z15.string()).optional()
9040
8739
  });
9041
- var mcpSseServerSchema = z14.object({
9042
- type: z14.literal("sse"),
9043
- url: z14.string().url(),
9044
- headers: z14.record(z14.string(), z14.string()).optional()
8740
+ var mcpSseServerSchema = z15.object({
8741
+ type: z15.literal("sse"),
8742
+ url: z15.string().url(),
8743
+ headers: z15.record(z15.string(), z15.string()).optional()
9045
8744
  });
9046
- var mcpServerDefSchema = z14.union([
8745
+ var mcpServerDefSchema = z15.union([
9047
8746
  mcpHttpServerSchema,
9048
8747
  mcpSseServerSchema,
9049
8748
  mcpStdioServerSchema
9050
8749
  ]);
9051
- var thinkingConfigSchema = z14.discriminatedUnion("type", [
9052
- z14.object({ type: z14.literal("adaptive") }),
9053
- z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
9054
- z14.object({ type: z14.literal("disabled") })
8750
+ var thinkingConfigSchema = z15.discriminatedUnion("type", [
8751
+ z15.object({ type: z15.literal("adaptive") }),
8752
+ z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
8753
+ z15.object({ type: z15.literal("disabled") })
9055
8754
  ]);
9056
- var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
9057
- var runConfigSchema = z14.object({
8755
+ var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
8756
+ var runConfigSchema = z15.object({
9058
8757
  // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
9059
8758
  // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
9060
8759
  // is the locked surface. Kept in lockstep with `@cabane/shared`'s
9061
8760
  // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
9062
8761
  // stripped, so an older companion riding a newer server never rejects the config).
9063
- hostAccess: z14.boolean().optional(),
9064
- mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
9065
- model: z14.string().min(1).optional(),
8762
+ hostAccess: z15.boolean().optional(),
8763
+ mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
8764
+ model: z15.string().min(1).optional(),
9066
8765
  thinking: thinkingConfigSchema.optional(),
9067
8766
  effort: effortSchema.optional()
9068
8767
  });
@@ -10153,22 +9852,22 @@ function handleUncaught(log, err, origin) {
10153
9852
  }
10154
9853
 
10155
9854
  // src/crash-marker.ts
10156
- import { existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync10, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
10157
- import { join as join18 } from "path";
9855
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
9856
+ import { join as join16 } from "path";
10158
9857
  function crashMarkerPath() {
10159
- return join18(cabaneDir(), "last-error.json");
9858
+ return join16(cabaneDir(), "last-error.json");
10160
9859
  }
10161
9860
  function recordCrash(rec2) {
10162
9861
  try {
10163
- mkdirSync13(cabaneDir(), { recursive: true });
10164
- writeFileSync9(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
9862
+ mkdirSync12(cabaneDir(), { recursive: true });
9863
+ writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
10165
9864
  } catch {
10166
9865
  }
10167
9866
  }
10168
9867
  function clearCrash() {
10169
9868
  try {
10170
9869
  const path = crashMarkerPath();
10171
- if (existsSync14(path)) rmSync9(path, { force: true });
9870
+ if (existsSync12(path)) rmSync8(path, { force: true });
10172
9871
  } catch {
10173
9872
  }
10174
9873
  }
@@ -10320,7 +10019,24 @@ async function closeSurfaces(control, dashboard) {
10320
10019
 
10321
10020
  // src/commands/daemon.ts
10322
10021
  import { spawn as spawn5 } from "child_process";
10323
- import { closeSync as closeSync3, mkdirSync as mkdirSync14, openSync as openSync3 } from "fs";
10022
+ import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
10023
+
10024
+ // src/cli-entry.ts
10025
+ import { existsSync as existsSync13 } from "fs";
10026
+ import { fileURLToPath as fileURLToPath2 } from "url";
10027
+ var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
10028
+ function companionCliEntry(deps = {}) {
10029
+ const exists = deps.exists ?? existsSync13;
10030
+ const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
10031
+ for (const candidate of candidates) {
10032
+ if (exists(candidate)) return candidate;
10033
+ }
10034
+ const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
10035
+ if (argv1 && exists(argv1)) return argv1;
10036
+ return candidates[0] ?? "";
10037
+ }
10038
+
10039
+ // src/commands/daemon.ts
10324
10040
  var STARTUP_TIMEOUT_MS = 8e3;
10325
10041
  var POLL_INTERVAL_MS = 150;
10326
10042
  async function startDaemon(opts = {}, deps = {}) {
@@ -10330,11 +10046,6 @@ async function startDaemon(opts = {}, deps = {}) {
10330
10046
  const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
10331
10047
  const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
10332
10048
  const now = deps.now ?? (() => Date.now());
10333
- const install = deps.installService ?? ((o) => installService(o));
10334
- const refresh = deps.refreshService ?? ((o) => refreshInstalledService(o));
10335
- const disable = deps.disableService ?? (() => disableService());
10336
- const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
10337
- const serviceOpts = {};
10338
10049
  const quiet = opts.report === "failures";
10339
10050
  const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
10340
10051
  // Unset → `requireStartConfig`'s own `requireConfig`, the real one.
@@ -10362,22 +10073,16 @@ async function startDaemon(opts = {}, deps = {}) {
10362
10073
  const existing = readState();
10363
10074
  if (existing) {
10364
10075
  if (await verify(existing) !== "stale") {
10365
- const rerendered = isTty() ? refresh(serviceOpts) : false;
10366
10076
  process.stdout.write(
10367
10077
  `Cabane Companion is already running (pid ${existing.pid}).
10368
10078
  Stop it first with \`cabane-companion stop\` if you want to relaunch.
10369
10079
  Connect a harness to the running companion: cabane-companion connect claude-code
10370
- ` + (rerendered ? "Autostart: the login service was updated for this install \u2014 it takes effect at the next login, or now with `cabane-companion stop` then `start`.\n" : "")
10080
+ `
10371
10081
  );
10372
- return {
10373
- started: false,
10374
- service: { installed: false, reason: "unsupported" },
10375
- state: existing
10376
- };
10082
+ return { started: false, state: existing };
10377
10083
  }
10378
10084
  clearRuntimeState();
10379
10085
  }
10380
- let service2 = isTty() ? install(serviceOpts) : { installed: false, reason: "unsupported" };
10381
10086
  const args = ["start", "--foreground"];
10382
10087
  const launchDetached = () => {
10383
10088
  const spawned = spawnDetached(args);
@@ -10394,29 +10099,8 @@ Connect a harness to the running companion: cabane-companion connect claude-code
10394
10099
  }
10395
10100
  return ready(seen) ? seen : null;
10396
10101
  };
10397
- if (!service2.installed && service2.leftBehind) {
10398
- refuseDouble(service2.leftBehind, service2.detail);
10399
- process.exitCode = 1;
10400
- return { started: false, service: service2, state: null };
10401
- }
10402
- let child = service2.installed ? null : launchDetached();
10403
- let state = await waitForReady();
10404
- if (!state && service2.installed) {
10405
- process.stdout.write(
10406
- `${service2.manager} started the companion but it didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 removing the login service.
10407
- `
10408
- );
10409
- const removed = disable();
10410
- if (!removed.handled || !removed.ok) {
10411
- refuseDouble(service2.manager, removed.detail);
10412
- process.exitCode = 1;
10413
- return { started: false, service: service2, state: null };
10414
- }
10415
- process.stdout.write("Launching it directly instead.\n");
10416
- service2 = { installed: false, reason: "command-failed", detail: "the service never came up" };
10417
- child = launchDetached();
10418
- state = await waitForReady();
10419
- }
10102
+ const child = launchDetached();
10103
+ const state = await waitForReady();
10420
10104
  if (!state) {
10421
10105
  process.stdout.write(
10422
10106
  `Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
@@ -10424,9 +10108,9 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
10424
10108
  `
10425
10109
  );
10426
10110
  process.exitCode = 1;
10427
- return { started: false, service: service2, state: null };
10111
+ return { started: false, state: null };
10428
10112
  }
10429
- if (quiet) return { started: true, service: service2, state };
10113
+ if (quiet) return { started: true, state };
10430
10114
  process.stdout.write(
10431
10115
  `Cabane Companion started in the background (pid ${state.pid}).
10432
10116
  Logs: ${companionLogPath()}
@@ -10434,27 +10118,11 @@ Status: cabane-companion status
10434
10118
  Stop: cabane-companion stop
10435
10119
  `
10436
10120
  );
10437
- if (service2.installed) {
10438
- process.stdout.write(`Autostart: enabled (${service2.manager}) \u2014 it starts again at login.
10439
- `);
10440
- } else if (service2.reason !== "unsupported") {
10441
- process.stdout.write(
10442
- `Autostart: not enabled \u2014 ${service2.detail ?? "the service manager refused the install"}. Running detached instead; it won't come back after a reboot.
10443
- `
10444
- );
10445
- }
10446
- return { started: true, service: service2, state };
10447
- }
10448
- function refuseDouble(manager, detail) {
10449
- process.stdout.write(
10450
- `${manager} still has the login service${detail ? ` (${detail})` : ""} \u2014 not launching a second companion beside a service that may still own one.
10451
- Check \`cabane-companion service status\`, then \`cabane-companion service disable\`, and run \`cabane-companion start --daemon\` again.
10452
- `
10453
- );
10121
+ return { started: true, state };
10454
10122
  }
10455
10123
  function defaultSpawnDetached(args) {
10456
10124
  const cliPath = companionCliEntry();
10457
- mkdirSync14(cabaneDir(), { recursive: true });
10125
+ mkdirSync13(cabaneDir(), { recursive: true });
10458
10126
  const logFd = openSync3(companionLogPath(), "a");
10459
10127
  try {
10460
10128
  return spawn5(process.execPath, [cliPath, ...args], {
@@ -10675,11 +10343,7 @@ async function handOffToBackground(runtime, ctx) {
10675
10343
  return;
10676
10344
  }
10677
10345
  if (ctx.spaceAbove) blank();
10678
- write(
10679
- INDENT + tick(
10680
- outcome.service.installed ? "Cabane companion is running in the background, and will start again when you log in." : "Cabane companion is running in the background."
10681
- )
10682
- );
10346
+ write(INDENT + tick("Cabane companion is running in the background."));
10683
10347
  write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
10684
10348
  if (ctx.justPaired) {
10685
10349
  blank();
@@ -10743,8 +10407,7 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
10743
10407
 
10744
10408
  // src/commands/status.ts
10745
10409
  import { readdirSync as readdirSync4 } from "fs";
10746
- async function status(deps = {}) {
10747
- const readService = deps.service ?? serviceStatus;
10410
+ async function status() {
10748
10411
  const cfg = loadConfig();
10749
10412
  if (!cfg || !cfg.deviceToken) {
10750
10413
  process.stdout.write(
@@ -10780,13 +10443,6 @@ async function status(deps = {}) {
10780
10443
  "companion: not running \u2014 `cabane-companion start` (backgrounds itself; `--foreground` stays attached)\n"
10781
10444
  );
10782
10445
  }
10783
- const svc = readService();
10784
- if (svc.installed) {
10785
- process.stdout.write(
10786
- `autostart: ${svc.manager} \u2014 starts at login` + (svc.manager === "systemd-user" && svc.linger !== "yes" ? ", while you are logged in" : "") + ` (details: cabane-companion service status)
10787
- `
10788
- );
10789
- }
10790
10446
  process.stdout.write(
10791
10447
  "agents: pulled from cabane at runtime \u2014 see this device in Cabane for its assigned agents, their run state, and any missing-secret warnings.\n"
10792
10448
  );
@@ -10833,34 +10489,12 @@ function formatUptime(startedAt) {
10833
10489
  // src/commands/stop.ts
10834
10490
  var TERM_GRACE_MS = 6e3;
10835
10491
  var POLL_INTERVAL_MS2 = 150;
10836
- var SERVICE_STOP_GRACE_MS = 3e3;
10837
10492
  async function stop(deps = {}) {
10838
10493
  const readState = deps.readState ?? readLiveRuntimeState;
10839
10494
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
10840
10495
  const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
10841
10496
  const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
10842
10497
  const now = deps.now ?? (() => Date.now());
10843
- const stopSvc = deps.stopService ?? (() => stopService());
10844
- const svc = stopSvc();
10845
- if (svc.handled) {
10846
- if (!svc.ok) {
10847
- process.stdout.write(
10848
- `companion: ${svc.manager} wouldn't stop the service${svc.detail ? ` (${svc.detail})` : ""}.
10849
- The service manager owns this process \u2014 stopping it directly would just be restarted.
10850
- Check \`cabane-companion service status\`, or remove it with \`cabane-companion service disable\`.
10851
- `
10852
- );
10853
- process.exitCode = 1;
10854
- return;
10855
- }
10856
- const deadline2 = now() + SERVICE_STOP_GRACE_MS;
10857
- while (readState() && now() < deadline2) await sleep4(POLL_INTERVAL_MS2);
10858
- if (!readState()) {
10859
- process.stdout.write(`companion: stopped (${svc.manager} service stopped).
10860
- `);
10861
- return;
10862
- }
10863
- }
10864
10498
  const state = readState();
10865
10499
  if (!state) {
10866
10500
  process.stdout.write("companion: not running (nothing to stop).\n");
@@ -10910,8 +10544,8 @@ function isAlive(kill, pid) {
10910
10544
  }
10911
10545
 
10912
10546
  // src/commands/transcript.ts
10913
- import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
10914
- import { isAbsolute, join as join19 } from "path";
10547
+ import { existsSync as existsSync14, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
10548
+ import { isAbsolute, join as join17 } from "path";
10915
10549
  async function transcript(opts = {}) {
10916
10550
  const dir2 = transcriptsDir();
10917
10551
  if (opts.follow) {
@@ -10928,7 +10562,7 @@ async function transcript(opts = {}) {
10928
10562
  process.stdout.write(emptyMessage(dir2));
10929
10563
  return;
10930
10564
  }
10931
- process.stdout.write(renderFile(join19(dir2, newest)) + "\n");
10565
+ process.stdout.write(renderFile(join17(dir2, newest)) + "\n");
10932
10566
  return;
10933
10567
  }
10934
10568
  printList(dir2);
@@ -11011,7 +10645,7 @@ function isComplete(content) {
11011
10645
  async function followTranscripts(dir2) {
11012
10646
  const follower = new TranscriptFollower({
11013
10647
  listFiles: () => listFiles(dir2),
11014
- read: (f) => readFileSync11(join19(dir2, f), "utf8"),
10648
+ read: (f) => readFileSync10(join17(dir2, f), "utf8"),
11015
10649
  write: (s) => process.stdout.write(s),
11016
10650
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
11017
10651
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -11051,7 +10685,7 @@ function printList(dir2) {
11051
10685
 
11052
10686
  `);
11053
10687
  for (const f of files.slice(0, 20)) {
11054
- const { meta, outcome } = peek(join19(dir2, f));
10688
+ const { meta, outcome } = peek(join17(dir2, f));
11055
10689
  const when = fmtTime(rec(meta)?.ts);
11056
10690
  const ws = str2(rec(meta)?.workspaceSlug);
11057
10691
  const o = rec(outcome);
@@ -11072,7 +10706,7 @@ function peek(path) {
11072
10706
  let meta;
11073
10707
  let outcome;
11074
10708
  try {
11075
- for (const line of readFileSync11(path, "utf8").split("\n")) {
10709
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
11076
10710
  if (!line.trim()) continue;
11077
10711
  const o = safeParse(line);
11078
10712
  const t = str2(rec(o)?.type);
@@ -11083,29 +10717,29 @@ function peek(path) {
11083
10717
  }
11084
10718
  return { meta, outcome };
11085
10719
  }
11086
- function resolveTarget(dir2, target2) {
11087
- if (isAbsolute(target2) || target2.includes("/")) {
11088
- if (existsSync15(target2)) return target2;
11089
- throw new CompanionError(`no transcript at ${target2}.`);
10720
+ function resolveTarget(dir2, target) {
10721
+ if (isAbsolute(target) || target.includes("/")) {
10722
+ if (existsSync14(target)) return target;
10723
+ throw new CompanionError(`no transcript at ${target}.`);
11090
10724
  }
11091
- const exact = join19(dir2, target2);
11092
- if (existsSync15(exact)) return exact;
11093
- const matches = listFiles(dir2).filter((f) => f.includes(target2));
11094
- if (matches.length === 1) return join19(dir2, matches[0]);
10725
+ const exact = join17(dir2, target);
10726
+ if (existsSync14(exact)) return exact;
10727
+ const matches = listFiles(dir2).filter((f) => f.includes(target));
10728
+ if (matches.length === 1) return join17(dir2, matches[0]);
11095
10729
  if (matches.length === 0) {
11096
10730
  throw new CompanionError(
11097
- `no transcript matching "${target2}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
10731
+ `no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
11098
10732
  );
11099
10733
  }
11100
10734
  throw new CompanionError(
11101
- `"${target2}" matches ${matches.length} transcripts \u2014 be more specific:
10735
+ `"${target}" matches ${matches.length} transcripts \u2014 be more specific:
11102
10736
  ` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
11103
10737
  );
11104
10738
  }
11105
10739
  function renderFile(path) {
11106
10740
  let content;
11107
10741
  try {
11108
- content = readFileSync11(path, "utf8");
10742
+ content = readFileSync10(path, "utf8");
11109
10743
  } catch (err) {
11110
10744
  throw new CompanionError(
11111
10745
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -11242,11 +10876,11 @@ program.command("pair").description(
11242
10876
  });
11243
10877
  });
11244
10878
  program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
11245
- writeCompletedPairing(readFileSync12(0, "utf8"));
10879
+ writeCompletedPairing(readFileSync11(0, "utf8"));
11246
10880
  });
11247
10881
  program.command("start").description(
11248
10882
  "pair this device if needed, connect a coding agent on it, and run in the background."
11249
- ).option("--foreground", "run attached in this terminal (never installs the login service)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(new Option("--open").hideHelp()).addOption(new Option("--no-open").hideHelp()).addOption(new Option("--port <port>").hideHelp()).action(async (opts) => {
10883
+ ).option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(new Option("--open").hideHelp()).addOption(new Option("--no-open").hideHelp()).addOption(new Option("--port <port>").hideHelp()).action(async (opts) => {
11250
10884
  await start({
11251
10885
  ...opts.foreground ? { foreground: true } : {},
11252
10886
  ...opts.server !== void 0 ? { server: opts.server } : {}
@@ -11261,13 +10895,6 @@ program.command("stop").description("stop a running companion (SIGTERM, then for
11261
10895
  program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
11262
10896
  await status();
11263
10897
  });
11264
- var service = program.command("service").description("inspect or remove the login service that starts the companion when you log in.");
11265
- service.command("status").description("print the service manager, whether the unit is installed and running.").action(() => {
11266
- serviceStatusCommand();
11267
- });
11268
- service.command("disable").description("stop the companion and remove the login service, leaving the machine clean.").action(() => {
11269
- serviceDisableCommand();
11270
- });
11271
10898
  program.command("transcript").description("show the full agent transcript for a recent dispatch (the agent's whole turn).").argument("[file]", "a transcript filename or substring; omit to list recent transcripts").option("--last", "render the most recent transcript").option("-f, --follow", "watch for new turns and live-render them as they land (Ctrl-C to stop)").action(async (file, opts) => {
11272
10899
  await transcript({
11273
10900
  ...file !== void 0 ? { target: file } : {},