@sechroom/cli 2026.7.30 → 2026.7.31

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 (2) hide show
  1. package/dist/index.js +2554 -506
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync13 } from "fs";
4
+ import { readFileSync as readFileSync15 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -724,7 +724,9 @@ Examples:
724
724
  $ sechroom id peek FR sechroom inspect the sequence without consuming
725
725
  $ sechroom id peek FR sechroom --json`
726
726
  );
727
- id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
727
+ id.command("next <namespaceKind> <scope>").description(
728
+ "Allocate the next id in a sequence (POST /id-registry/allocate)"
729
+ ).action(async (namespaceKind, scope, _opts, cmd) => {
728
730
  const cfg = resolveConfig(cmd.optsWithGlobals());
729
731
  const data = await runApi("Allocating id", async () => {
730
732
  const client = await makeClient(cfg);
@@ -732,9 +734,15 @@ Examples:
732
734
  body: { namespaceKind, scope, clientNonce: null }
733
735
  });
734
736
  });
735
- emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
737
+ emitAction(
738
+ `allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`,
739
+ data,
740
+ cmd.optsWithGlobals().json
741
+ );
736
742
  });
737
- id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
743
+ id.command("peek <namespaceKind> <scope>").description(
744
+ "Inspect a sequence without consuming (GET /id-registry/state)"
745
+ ).action(async (namespaceKind, scope, _opts, cmd) => {
738
746
  const cfg = resolveConfig(cmd.optsWithGlobals());
739
747
  const data = await runApi("Peeking id sequence", async () => {
740
748
  const client = await makeClient(cfg);
@@ -780,7 +788,13 @@ Examples:
780
788
  });
781
789
  emitAction("updated profile", data, cmd.optsWithGlobals().json);
782
790
  });
783
- account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).option("--since <iso>", "Only contributions updated since this ISO-8601 timestamp (updatedSince)").option("--order <order>", "Order: LastTouchedDesc | LastTouchedAsc | FirstTouchedDesc | FirstTouchedAsc").action(async (opts, cmd) => {
791
+ account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).option(
792
+ "--since <iso>",
793
+ "Only contributions updated since this ISO-8601 timestamp (updatedSince)"
794
+ ).option(
795
+ "--order <order>",
796
+ "Order: LastTouchedDesc | LastTouchedAsc | FirstTouchedDesc | FirstTouchedAsc"
797
+ ).action(async (opts, cmd) => {
784
798
  const cfg = resolveConfig(cmd.optsWithGlobals());
785
799
  const data = await runApi("Fetching feed", async () => {
786
800
  const client = await makeClient(cfg);
@@ -822,7 +836,9 @@ Examples:
822
836
  const cfg = resolveConfig(cmd.optsWithGlobals());
823
837
  const data = await runApi("Fetching review", async () => {
824
838
  const client = await makeClient(cfg);
825
- return client.GET("/reviews/{reviewId}", { params: { path: { reviewId } } });
839
+ return client.GET("/reviews/{reviewId}", {
840
+ params: { path: { reviewId } }
841
+ });
826
842
  });
827
843
  emit(data, cmd.optsWithGlobals().json);
828
844
  });
@@ -835,13 +851,19 @@ Examples:
835
851
  body: { decisions: {} }
836
852
  });
837
853
  });
838
- emitAction(`accepted review ${style.bold(reviewId)}`, data, cmd.optsWithGlobals().json);
854
+ emitAction(
855
+ `accepted review ${style.bold(reviewId)}`,
856
+ data,
857
+ cmd.optsWithGlobals().json
858
+ );
839
859
  });
840
860
  account.command("lookup-batch <ids...>").description("Resolve many ids at once (POST /lookup/batch)").action(async (ids, _opts, cmd) => {
841
861
  const cfg = resolveConfig(cmd.optsWithGlobals());
842
862
  const data = await runApi(`Resolving ${ids.length} ids`, async () => {
843
863
  const client = await makeClient(cfg);
844
- return client.POST("/lookup/batch", { body: { ids } });
864
+ return client.POST("/lookup/batch", {
865
+ body: { ids, includeArchived: false }
866
+ });
845
867
  });
846
868
  emit(data, cmd.optsWithGlobals().json);
847
869
  });
@@ -1542,8 +1564,8 @@ target:gpt-codex-agent), the dispatchable workers your loop skills call
1542
1564
  }
1543
1565
 
1544
1566
  // src/commands/channel.ts
1545
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
1546
- import { dirname as dirname8, join as join11 } from "path";
1567
+ import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
1568
+ import { dirname as dirname9, join as join12 } from "path";
1547
1569
  import {
1548
1570
  HttpTransportType,
1549
1571
  HubConnectionBuilder
@@ -1552,8 +1574,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1552
1574
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1553
1575
 
1554
1576
  // src/commands/executor.ts
1555
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
1556
- import { dirname as dirname7, join as join10 } from "path";
1577
+ import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
1578
+ import { dirname as dirname8, join as join11 } from "path";
1557
1579
 
1558
1580
  // src/sem.ts
1559
1581
  import { dirname as dirname2, join as join5 } from "path";
@@ -1706,8 +1728,8 @@ function ensureSemIgnored(semPath) {
1706
1728
  if (target.exists) {
1707
1729
  const content = readFileSync3(target.path, "utf8");
1708
1730
  if (ignoresSem(content)) return;
1709
- const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1710
- appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1731
+ const sep2 = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1732
+ appendFileSync(target.path, `${sep2}${STATE_DIR_IGNORE}
1711
1733
  `);
1712
1734
  } else {
1713
1735
  writeFileSync4(target.path, `${STATE_DIR_IGNORE}
@@ -1718,74 +1740,87 @@ function ensureSemIgnored(semPath) {
1718
1740
  }
1719
1741
 
1720
1742
  // src/commands/executor-run.ts
1721
- import { join as join9, resolve as resolve2 } from "path";
1743
+ import { join as join10, resolve as resolve3 } from "path";
1722
1744
 
1723
- // src/executor-run/claim.ts
1724
- var SUCCESS = /* @__PURE__ */ new Set(["Claimed", "AlreadyHeld"]);
1725
- async function claimNextTask(deps) {
1726
- const { request, executorInstanceId } = deps;
1727
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
1728
- const idempotencyKey = deps.idempotencyKey ?? ((offer) => `executor-run:${offer.generationId}`);
1729
- const tokenVersion = deps.tokenVersion ?? 1;
1730
- const log = deps.log ?? (() => {
1745
+ // src/executor-run/request.ts
1746
+ var AuthExpiredError = class extends Error {
1747
+ constructor(message) {
1748
+ super(message);
1749
+ this.name = "AuthExpiredError";
1750
+ }
1751
+ };
1752
+ var HttpError = class extends Error {
1753
+ constructor(status, method, path, body) {
1754
+ super(`${method} ${path} failed (${status}): ${body}`);
1755
+ this.status = status;
1756
+ this.method = method;
1757
+ this.path = path;
1758
+ this.body = body;
1759
+ this.name = "HttpError";
1760
+ }
1761
+ status;
1762
+ method;
1763
+ path;
1764
+ body;
1765
+ };
1766
+ function createAuthedRequest(cfg, deps = {}) {
1767
+ const getToken = deps.getToken ?? requireToken;
1768
+ const refresh = deps.refreshToken ?? forceRefreshToken;
1769
+ const doFetch = deps.fetch ?? fetch;
1770
+ const call = async (path, init, token) => doFetch(`${cfg.baseUrl}${path}`, {
1771
+ ...init,
1772
+ headers: {
1773
+ authorization: `Bearer ${token}`,
1774
+ tenant: cfg.tenant,
1775
+ "content-type": "application/json",
1776
+ "x-sechroom-surface": "cli",
1777
+ ...init?.headers
1778
+ }
1731
1779
  });
1732
- const base = `/me/executor-instances/${encodeURIComponent(executorInstanceId)}`;
1733
- const offers = await request(`${base}/dispatch-offers`);
1734
- if (offers.length === 0) return null;
1735
- const head = offers[0];
1736
- if (head.suggestedClaimDelayMs > 0) await sleep(head.suggestedClaimDelayMs);
1737
- const next = await request(
1738
- `${base}/dispatch-offers/claim-next`,
1739
- {
1740
- method: "POST",
1741
- body: JSON.stringify({ idempotencyKey: idempotencyKey(head) })
1780
+ return async (path, init) => {
1781
+ const method = init?.method ?? "GET";
1782
+ let token;
1783
+ try {
1784
+ token = await getToken(cfg);
1785
+ } catch (error) {
1786
+ throw new AuthExpiredError(
1787
+ error instanceof Error ? error.message : String(error)
1788
+ );
1742
1789
  }
1743
- );
1744
- if (SUCCESS.has(next.outcome)) return toClaimed(next, tokenVersion, next.offer ?? head);
1745
- if (next.outcome === "NoOffer") return null;
1746
- log(
1747
- `claim-next returned ${next.outcome}; falling through to direct per-generation claim`
1748
- );
1749
- for (const offer of offers) {
1750
- const direct = await request(`/me/executor-task-claims`, {
1751
- method: "POST",
1752
- body: JSON.stringify({
1753
- generationId: offer.generationId,
1754
- executorInstanceId
1755
- })
1756
- });
1757
- if (SUCCESS.has(direct.outcome)) return toClaimed(direct, tokenVersion, offer);
1758
- }
1759
- return null;
1760
- }
1761
- function toClaimed(result, tokenVersion, offer) {
1762
- const lease = result.lease;
1763
- const claimToken = result.claimToken;
1764
- if (!lease?.id || !claimToken) return null;
1765
- return {
1766
- outcome: result.outcome,
1767
- leaseId: lease.id,
1768
- claimToken,
1769
- tokenVersion,
1770
- memoryId: lease.memoryId,
1771
- workspaceId: lease.workspaceId,
1772
- generationId: lease.generationId,
1773
- decompositionId: decompositionIdFrom(offer?.tags ?? [])
1790
+ let response = await call(path, init, token);
1791
+ if (response.status === 401) {
1792
+ let fresh;
1793
+ try {
1794
+ fresh = await refresh(cfg);
1795
+ } catch (error) {
1796
+ throw new AuthExpiredError(
1797
+ error instanceof Error ? error.message : String(error)
1798
+ );
1799
+ }
1800
+ response = await call(path, init, fresh);
1801
+ if (response.status === 401)
1802
+ throw new AuthExpiredError(
1803
+ `${method} ${path} still 401 after token refresh \u2014 re-authenticate (\`sechroom login\`).`
1804
+ );
1805
+ }
1806
+ if (!response.ok)
1807
+ throw new HttpError(
1808
+ response.status,
1809
+ method,
1810
+ path,
1811
+ await safeText(response)
1812
+ );
1813
+ return await response.json();
1774
1814
  };
1775
1815
  }
1776
- function decompositionIdFrom(tags) {
1777
- const tag = tags.find((value) => value.startsWith("wlp-decomposition:"));
1778
- return tag?.slice("wlp-decomposition:".length) || void 0;
1816
+ async function safeText(response) {
1817
+ try {
1818
+ return await response.text();
1819
+ } catch {
1820
+ return "";
1821
+ }
1779
1822
  }
1780
1823
 
1781
- // src/executor-run/codex.ts
1782
- import {
1783
- execFile,
1784
- spawn
1785
- } from "child_process";
1786
- import { createInterface } from "readline";
1787
- import { promisify } from "util";
1788
-
1789
1824
  // src/executor-run/usage.ts
1790
1825
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
1791
1826
  import { dirname as dirname3 } from "path";
@@ -1804,11 +1839,21 @@ function parseWindow(value) {
1804
1839
  if (!obj) return void 0;
1805
1840
  const usedPercent = numberOf(obj, "used_percent", "usedPercent");
1806
1841
  if (usedPercent === void 0) return void 0;
1807
- return {
1842
+ const parsed = {
1808
1843
  usedPercent,
1809
- windowMinutes: numberOf(obj, "window_minutes", "windowMinutes"),
1844
+ windowMinutes: numberOf(
1845
+ obj,
1846
+ "window_minutes",
1847
+ "windowMinutes",
1848
+ "window_duration_mins",
1849
+ "windowDurationMins"
1850
+ ),
1810
1851
  resetsInSeconds: numberOf(obj, "resets_in_seconds", "resetsInSeconds")
1811
1852
  };
1853
+ const resetsAtUnixSeconds = numberOf(obj, "resets_at", "resetsAt");
1854
+ if (resetsAtUnixSeconds !== void 0)
1855
+ parsed.resetsAtUnixSeconds = resetsAtUnixSeconds;
1856
+ return parsed;
1812
1857
  }
1813
1858
  function object(value) {
1814
1859
  return value && typeof value === "object" ? value : void 0;
@@ -1967,33 +2012,321 @@ function createUsageLogAppender(path, log) {
1967
2012
  };
1968
2013
  }
1969
2014
 
2015
+ // src/executor-run/account-capacity.ts
2016
+ var CODEX_CAPACITY_LEG = "codex-capacity";
2017
+ var CODEX_CAPACITY_SOURCE = "codex-app-server:account/rateLimits/read";
2018
+ var CODEX_WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
2019
+ function normalizeCodexAccountCapacity(signal, nowMs) {
2020
+ const account = object2(object2(signal.accountResponse)?.account);
2021
+ const email = account?.type === "chatgpt" && typeof account.email === "string" ? account.email.trim().toLowerCase() : "";
2022
+ if (!email) return void 0;
2023
+ const response = object2(signal.rateLimitsResponse);
2024
+ const byLimitId = object2(response?.rateLimitsByLimitId);
2025
+ const topLevelLimits = object2(response?.rateLimits);
2026
+ const limits = object2(byLimitId?.codex) ?? topLevelLimits;
2027
+ if (!limits) return void 0;
2028
+ const parsed = parseRateLimitPayload(limits);
2029
+ const credits = object2(limits.credits) ?? object2(topLevelLimits?.credits) ?? object2(response?.credits);
2030
+ const hasCreditsValue = credits?.hasCredits ?? credits?.has_credits;
2031
+ const hasCredits = typeof hasCreditsValue === "boolean" ? hasCreditsValue : null;
2032
+ const weekly = [parsed?.primary, parsed?.secondary].find(
2033
+ (window) => window?.windowMinutes === CODEX_WEEKLY_WINDOW_MINUTES
2034
+ );
2035
+ if (!weekly) return void 0;
2036
+ const weeklyPctUsed = Math.max(0, Math.min(100, weekly.usedPercent));
2037
+ const resetAtMs = weekly.resetsAtUnixSeconds !== void 0 ? weekly.resetsAtUnixSeconds * 1e3 : weekly.resetsInSeconds !== void 0 ? nowMs + weekly.resetsInSeconds * 1e3 : void 0;
2038
+ return {
2039
+ leg: CODEX_CAPACITY_LEG,
2040
+ accountId: `codex:chatgpt:${email}`,
2041
+ weeklyPctUsed,
2042
+ weeklyPctLeft: 100 - weeklyPctUsed,
2043
+ hasCredits,
2044
+ windowResetAt: resetAtMs === void 0 ? null : new Date(resetAtMs).toISOString(),
2045
+ capturedAt: new Date(nowMs).toISOString(),
2046
+ source: CODEX_CAPACITY_SOURCE
2047
+ };
2048
+ }
2049
+ async function captureCodexAccountCapacityOnce(options) {
2050
+ const signal = await options.read();
2051
+ const snapshot = normalizeCodexAccountCapacity(
2052
+ signal,
2053
+ options.now?.() ?? Date.now()
2054
+ );
2055
+ if (!snapshot) {
2056
+ if (object2(object2(signal.accountResponse)?.account)?.type === "apiKey") {
2057
+ options.log(
2058
+ "codex capacity capture disabled for API-key auth \u2014 codex:api-key:<fingerprint> is reserved until Codex exposes an attributable fingerprint"
2059
+ );
2060
+ return void 0;
2061
+ }
2062
+ options.log(
2063
+ "codex capacity read carried no attributable weekly window \u2014 snapshot skipped"
2064
+ );
2065
+ return void 0;
2066
+ }
2067
+ await options.persist(snapshot);
2068
+ options.log(
2069
+ `codex capacity captured \u2014 ${snapshot.accountId}, ${snapshot.weeklyPctLeft.toFixed(1)}% weekly left`
2070
+ );
2071
+ return snapshot;
2072
+ }
2073
+ async function runCodexAccountCapacityTick(options) {
2074
+ try {
2075
+ await captureCodexAccountCapacityOnce(options);
2076
+ } catch (error) {
2077
+ if (error instanceof AuthExpiredError) throw error;
2078
+ options.log(
2079
+ `codex capacity capture failed (${error instanceof Error ? error.message : String(error)}) \u2014 retrying next interval`
2080
+ );
2081
+ }
2082
+ }
2083
+ function startCodexAccountCapacityCapture(options) {
2084
+ const timers = options.timers ?? { setInterval, clearInterval };
2085
+ let stopped = false;
2086
+ let running = false;
2087
+ let apiKeyNoticeLogged = false;
2088
+ let timer;
2089
+ const stop = () => {
2090
+ if (stopped) return;
2091
+ stopped = true;
2092
+ if (timer !== void 0) timers.clearInterval(timer);
2093
+ };
2094
+ const captureOptions = {
2095
+ ...options,
2096
+ log: (line) => {
2097
+ if (line.startsWith("codex capacity capture disabled for API-key auth")) {
2098
+ if (apiKeyNoticeLogged) return;
2099
+ apiKeyNoticeLogged = true;
2100
+ }
2101
+ options.log(line);
2102
+ }
2103
+ };
2104
+ const tick = async () => {
2105
+ if (stopped || running) return;
2106
+ running = true;
2107
+ try {
2108
+ await runCodexAccountCapacityTick(captureOptions);
2109
+ } catch (error) {
2110
+ if (error instanceof AuthExpiredError) {
2111
+ stop();
2112
+ try {
2113
+ options.onTerminal(error);
2114
+ } catch (callbackError) {
2115
+ options.log(
2116
+ `codex capacity terminal callback failed (${callbackError instanceof Error ? callbackError.message : String(callbackError)})`
2117
+ );
2118
+ }
2119
+ }
2120
+ } finally {
2121
+ running = false;
2122
+ }
2123
+ };
2124
+ timer = timers.setInterval(() => void tick(), options.intervalMs);
2125
+ timer.unref?.();
2126
+ void tick();
2127
+ return stop;
2128
+ }
2129
+ async function postCodexAccountCapacity(request, snapshot) {
2130
+ await request("/work-layer/telemetry/codex-account-capacity", {
2131
+ method: "POST",
2132
+ body: JSON.stringify(snapshot)
2133
+ });
2134
+ }
2135
+ function object2(value) {
2136
+ return value && typeof value === "object" ? value : void 0;
2137
+ }
2138
+
2139
+ // src/executor-run/claim.ts
2140
+ var SUCCESS = /* @__PURE__ */ new Set(["Claimed", "AlreadyHeld"]);
2141
+ var CONTENTION_STATUSES = /* @__PURE__ */ new Set([409, 410]);
2142
+ async function claimNextTask(deps) {
2143
+ const { request, executorInstanceId } = deps;
2144
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
2145
+ const idempotencyKey = deps.idempotencyKey ?? ((offer) => `executor-run:${offer.generationId}`);
2146
+ const tokenVersion = deps.tokenVersion ?? 1;
2147
+ const log = deps.log ?? (() => {
2148
+ });
2149
+ const excludeTags = deps.excludeTags ?? [];
2150
+ const base = `/me/executor-instances/${encodeURIComponent(executorInstanceId)}`;
2151
+ const offers = await request(`${base}/dispatch-offers`);
2152
+ if (offers.length === 0) return null;
2153
+ const eligible = [];
2154
+ let anyExcluded = false;
2155
+ for (const offer of offers) {
2156
+ const hit = excludeTags.find((tag) => offer.tags.includes(tag));
2157
+ if (hit !== void 0) {
2158
+ anyExcluded = true;
2159
+ const key = `${offer.memoryId}:${offer.generationId}`;
2160
+ if (!deps.skipLog?.has(key)) {
2161
+ deps.skipLog?.add(key);
2162
+ log(`skipped ${offer.memoryId} (excluded tag ${hit})`);
2163
+ }
2164
+ continue;
2165
+ }
2166
+ eligible.push(offer);
2167
+ }
2168
+ if (eligible.length === 0) return null;
2169
+ if (!anyExcluded) {
2170
+ const head = eligible[0];
2171
+ if (head.suggestedClaimDelayMs > 0) await sleep(head.suggestedClaimDelayMs);
2172
+ let next;
2173
+ try {
2174
+ next = await request(
2175
+ `${base}/dispatch-offers/claim-next`,
2176
+ {
2177
+ method: "POST",
2178
+ body: JSON.stringify({ idempotencyKey: idempotencyKey(head) })
2179
+ }
2180
+ );
2181
+ } catch (error) {
2182
+ if (!(error instanceof HttpError && CONTENTION_STATUSES.has(error.status)))
2183
+ throw error;
2184
+ log(
2185
+ `claim-next lost the race (HTTP ${error.status}); falling through to direct per-generation claim`
2186
+ );
2187
+ }
2188
+ if (next) {
2189
+ if (SUCCESS.has(next.outcome))
2190
+ return toClaimed(next, next.tokenVersion ?? tokenVersion);
2191
+ if (next.outcome === "NoOffer") return null;
2192
+ log(
2193
+ `claim-next returned ${next.outcome}; falling through to direct per-generation claim`
2194
+ );
2195
+ }
2196
+ }
2197
+ for (const offer of eligible) {
2198
+ let direct;
2199
+ try {
2200
+ direct = await request(`/me/executor-task-claims`, {
2201
+ method: "POST",
2202
+ body: JSON.stringify({
2203
+ generationId: offer.generationId,
2204
+ executorInstanceId
2205
+ })
2206
+ });
2207
+ } catch (error) {
2208
+ if (error instanceof HttpError && CONTENTION_STATUSES.has(error.status)) {
2209
+ log(
2210
+ `direct claim ${offer.generationId} lost the race (HTTP ${error.status}); trying the next offer`
2211
+ );
2212
+ continue;
2213
+ }
2214
+ throw error;
2215
+ }
2216
+ if (SUCCESS.has(direct.outcome))
2217
+ return toClaimed(direct, direct.tokenVersion ?? tokenVersion);
2218
+ }
2219
+ return null;
2220
+ }
2221
+ function toClaimed(result, tokenVersion) {
2222
+ const lease = result.lease;
2223
+ const claimToken = result.claimToken;
2224
+ if (!lease?.id || !claimToken) return null;
2225
+ return {
2226
+ outcome: result.outcome,
2227
+ leaseId: lease.id,
2228
+ claimToken,
2229
+ tokenVersion,
2230
+ memoryId: lease.memoryId,
2231
+ workspaceId: lease.workspaceId,
2232
+ generationId: lease.generationId,
2233
+ decompositionId: lease.decompositionId ?? void 0
2234
+ };
2235
+ }
2236
+
1970
2237
  // src/executor-run/codex.ts
2238
+ import {
2239
+ execFile,
2240
+ spawn
2241
+ } from "child_process";
2242
+ import { createInterface } from "readline";
2243
+ import { promisify } from "util";
1971
2244
  var execFileAsync = promisify(execFile);
2245
+ var DEFAULT_ACCOUNT_READ_TIMEOUT_MS = 3e4;
1972
2246
  function shouldResumeTimedOutTurn(input) {
1973
2247
  return input.outcome === "timeout" && !input.hasPacket && Boolean(input.threadId) && (input.contextUsed > 0 || input.detailEventCount > 0 || input.lastAgentMessage.length > 0);
1974
2248
  }
1975
2249
  function parseCodexUsage(params, previous) {
1976
- const info = object2(params.tokenUsageInfo) ?? params;
1977
- const last = object2(info.last) ?? object2(info.lastTokenUsage) ?? object2(info.last_turn);
1978
- const total = object2(info.total) ?? object2(info.totalTokenUsage) ?? info;
2250
+ const info = object3(params.tokenUsageInfo) ?? object3(params.tokenUsage) ?? object3(params.token_usage) ?? params;
2251
+ const last = object3(info.last) ?? object3(info.lastTokenUsage) ?? object3(info.last_token_usage) ?? object3(info.last_turn);
2252
+ const total = object3(info.total) ?? object3(info.totalTokenUsage) ?? object3(info.total_token_usage) ?? info;
1979
2253
  const resumed = Boolean(params.resumed ?? info.resumed ?? last);
1980
2254
  const spend = resumed && last ? last : total;
1981
2255
  const tokensIn = numberOf2(spend, "input_tokens", "inputTokens", "input") ?? previous?.tokensIn ?? 0;
1982
2256
  const tokensOut = numberOf2(spend, "output_tokens", "outputTokens", "output") ?? previous?.tokensOut ?? 0;
1983
2257
  const contextUsed = numberOf2(last ?? spend, "total_tokens", "totalTokens", "total") ?? tokensIn + tokensOut;
1984
- const contextWindow = numberOf2(params, "modelContextWindow", "contextWindow") ?? numberOf2(info, "modelContextWindow", "contextWindow") ?? previous?.contextWindow ?? 0;
2258
+ const contextWindow = numberOf2(
2259
+ params,
2260
+ "modelContextWindow",
2261
+ "contextWindow",
2262
+ "model_context_window"
2263
+ ) ?? numberOf2(
2264
+ info,
2265
+ "modelContextWindow",
2266
+ "contextWindow",
2267
+ "model_context_window"
2268
+ ) ?? previous?.contextWindow ?? 0;
1985
2269
  const modelId = stringOf(params, "model", "modelId") ?? stringOf(info, "model", "modelId") ?? previous?.modelId ?? null;
1986
- return { tokensIn, tokensOut, contextUsed, contextWindow, modelId };
2270
+ const cachedInputTokens = numberOf2(spend, "cached_input_tokens", "cachedInputTokens") ?? previous?.cachedInputTokens ?? null;
2271
+ const reasoningEffort = stringOf(params, "reasoning_effort", "reasoningEffort") ?? stringOf(info, "reasoning_effort", "reasoningEffort") ?? previous?.reasoningEffort ?? null;
2272
+ const rateLimits = parseRateLimitPayload(params);
2273
+ const remainingAllowance = (rateLimits ? rateLimitRemainingFraction(rateLimits) : void 0) ?? previous?.remainingAllowance ?? null;
2274
+ return {
2275
+ tokensIn,
2276
+ tokensOut,
2277
+ contextUsed,
2278
+ contextWindow,
2279
+ modelId,
2280
+ cachedInputTokens,
2281
+ reasoningEffort,
2282
+ remainingAllowance
2283
+ };
1987
2284
  }
1988
- function object2(value) {
2285
+ function rateLimitRemainingFraction(payload) {
2286
+ let remaining;
2287
+ for (const window of [payload.primary, payload.secondary]) {
2288
+ if (!window) continue;
2289
+ const fraction = Math.max(0, Math.min(100, 100 - window.usedPercent)) / 100;
2290
+ remaining = remaining === void 0 ? fraction : Math.min(remaining, fraction);
2291
+ }
2292
+ return remaining;
2293
+ }
2294
+ function emptyCodexUsage() {
2295
+ return {
2296
+ tokensIn: 0,
2297
+ tokensOut: 0,
2298
+ contextUsed: 0,
2299
+ contextWindow: 0,
2300
+ modelId: null,
2301
+ cachedInputTokens: null,
2302
+ reasoningEffort: null,
2303
+ remainingAllowance: null
2304
+ };
2305
+ }
2306
+ function mergeCodexThreadStartUsage(started, previous) {
2307
+ const normalized = parseCodexUsage(started, previous);
2308
+ const modelIdentified = normalized.modelId !== (previous?.modelId ?? null);
2309
+ const reasoningEffort = stringOf(started, "reasoning_effort", "reasoningEffort") ?? stringOf(
2310
+ object3(started.thread) ?? {},
2311
+ "reasoning_effort",
2312
+ "reasoningEffort"
2313
+ );
2314
+ return reasoningEffort === void 0 && !modelIdentified ? previous : {
2315
+ ...normalized,
2316
+ reasoningEffort: reasoningEffort ?? normalized.reasoningEffort
2317
+ };
2318
+ }
2319
+ function object3(value) {
1989
2320
  return value && typeof value === "object" ? value : void 0;
1990
2321
  }
1991
2322
  function numberOf2(obj, ...keys) {
1992
- for (const key of keys) if (typeof obj[key] === "number") return obj[key];
2323
+ for (const key of keys)
2324
+ if (typeof obj[key] === "number") return obj[key];
1993
2325
  return void 0;
1994
2326
  }
1995
2327
  function stringOf(obj, ...keys) {
1996
- for (const key of keys) if (typeof obj[key] === "string") return obj[key];
2328
+ for (const key of keys)
2329
+ if (typeof obj[key] === "string") return obj[key];
1997
2330
  return void 0;
1998
2331
  }
1999
2332
  function renderUsage(usage) {
@@ -2005,13 +2338,19 @@ function mapThreadItem(msg) {
2005
2338
  const type = String(msg.params?.type ?? msg.params?.itemType ?? method);
2006
2339
  const text2 = JSON.stringify({ method, type, id: msg.params?.id ?? null });
2007
2340
  if (/approval/i.test(type)) return { kind: "Approval", text: text2 };
2008
- if (/commandExecution|fileChange|contextCompaction|agentMessage|userMessage/i.test(type))
2009
- return { kind: /agentMessage|userMessage/i.test(type) ? "Raw" : "Parsed", text: text2 };
2341
+ if (/commandExecution|fileChange|contextCompaction|agentMessage|userMessage/i.test(
2342
+ type
2343
+ ))
2344
+ return {
2345
+ kind: /agentMessage|userMessage/i.test(type) ? "Raw" : "Parsed",
2346
+ text: text2
2347
+ };
2010
2348
  return void 0;
2011
2349
  }
2012
2350
  function verdictForTelemetry(status) {
2013
2351
  if (status === "completed") return "pass";
2014
- if (status === "needs_approval" || status === "cancelled" || status === "canceled") return "blocked";
2352
+ if (status === "needs_approval" || status === "cancelled" || status === "canceled")
2353
+ return "blocked";
2015
2354
  return "soft-fail";
2016
2355
  }
2017
2356
  var CodexAppServer = class {
@@ -2026,10 +2365,14 @@ var CodexAppServer = class {
2026
2365
  onNotification;
2027
2366
  exited;
2028
2367
  cliVersion;
2368
+ warnedZeroUsage = false;
2029
2369
  /** Spawn the child + initialize. Idempotent per instance. */
2030
2370
  async start() {
2031
2371
  if (this.child) return;
2032
- this.cliVersion ??= await pinCodexVersion(this.options.codexBin, this.options.log);
2372
+ this.cliVersion ??= await pinCodexVersion(
2373
+ this.options.codexBin,
2374
+ this.options.log
2375
+ );
2033
2376
  const child = spawn(this.options.codexBin, ["app-server", "--stdio"], {
2034
2377
  cwd: this.options.cwd,
2035
2378
  stdio: ["pipe", "pipe", "pipe"]
@@ -2037,7 +2380,9 @@ var CodexAppServer = class {
2037
2380
  this.child = child;
2038
2381
  child.on("exit", (code) => {
2039
2382
  this.exited = { code };
2040
- const failure = new Error(`codex app-server exited (${code ?? "signal"})`);
2383
+ const failure = new Error(
2384
+ `codex app-server exited (${code ?? "signal"})`
2385
+ );
2041
2386
  for (const waiter of this.pending.values()) waiter.reject(failure);
2042
2387
  this.pending.clear();
2043
2388
  });
@@ -2057,6 +2402,15 @@ var CodexAppServer = class {
2057
2402
  get alive() {
2058
2403
  return this.child !== void 0 && this.exited === void 0;
2059
2404
  }
2405
+ /** Read the same authenticated account + weekly capacity signal Codex panes render. */
2406
+ async readAccountCapacitySignal() {
2407
+ const timeoutMs = this.options.accountReadTimeoutMs ?? DEFAULT_ACCOUNT_READ_TIMEOUT_MS;
2408
+ const [accountResponse, rateLimitsResponse] = await Promise.all([
2409
+ this.request("account/read", {}, timeoutMs),
2410
+ this.request("account/rateLimits/read", void 0, timeoutMs)
2411
+ ]);
2412
+ return { accountResponse, rateLimitsResponse };
2413
+ }
2060
2414
  /**
2061
2415
  * Run one task as one thread+turn; resolves when the server reports
2062
2416
  * `turn/completed` (a server→client REQUEST, per the Looper-verified protocol),
@@ -2068,6 +2422,7 @@ var CodexAppServer = class {
2068
2422
  let threadId = "";
2069
2423
  let turnId = "";
2070
2424
  let usage;
2425
+ let fileChangeCount = 0;
2071
2426
  const detailEvents = [];
2072
2427
  const modelId = this.options.model ?? null;
2073
2428
  const event = (kind, text2 = null) => ({
@@ -2088,20 +2443,40 @@ var CodexAppServer = class {
2088
2443
  this.onNotification = (msg) => {
2089
2444
  if (msg.method === "thread/tokenUsage/updated") {
2090
2445
  usage = parseCodexUsage(msg.params ?? {}, usage);
2446
+ if (!this.warnedZeroUsage && usage.tokensIn === 0 && usage.tokensOut === 0 && usage.contextUsed === 0) {
2447
+ this.warnedZeroUsage = true;
2448
+ this.options.log(
2449
+ `usage parsed all-zero \u2014 raw payload: ${JSON.stringify(msg.params ?? {}).slice(0, 400)}`
2450
+ );
2451
+ }
2091
2452
  this.options.log(renderUsage(usage));
2092
2453
  this.options.onUsage?.(telemetry?.taskId ?? "", usage);
2093
2454
  this.tapRateLimits(msg.params ?? {});
2094
2455
  return;
2095
2456
  }
2457
+ if (msg.method && rateLimitMethod(msg.method)) {
2458
+ const limits = parseRateLimitPayload(msg.params ?? {});
2459
+ const allowance = limits ? rateLimitRemainingFraction(limits) : void 0;
2460
+ if (allowance !== void 0) {
2461
+ usage = {
2462
+ ...usage ?? emptyCodexUsage(),
2463
+ remainingAllowance: allowance
2464
+ };
2465
+ }
2466
+ }
2096
2467
  const mapped = mapThreadItem(msg);
2097
2468
  if (mapped) detailEvents.push(event(mapped.kind, mapped.text));
2469
+ if (/fileChange/i.test(
2470
+ `${msg.method ?? ""} ${String(msg.params?.type ?? msg.params?.itemType ?? "")}`
2471
+ ))
2472
+ fileChangeCount += 1;
2098
2473
  };
2099
- const terminal = new Promise((resolve5) => {
2474
+ const terminal = new Promise((resolve6) => {
2100
2475
  this.onServerRequest = (msg) => {
2101
2476
  const method = msg.method ?? "";
2102
2477
  if (terminalMethod(method)) {
2103
2478
  this.respond(msg.id, {});
2104
- resolve5("completed");
2479
+ resolve6("completed");
2105
2480
  return;
2106
2481
  }
2107
2482
  if (method === "item/tool/call") {
@@ -2117,19 +2492,29 @@ var CodexAppServer = class {
2117
2492
  return;
2118
2493
  }
2119
2494
  if (name === "sechroom_lifecycle_signal") {
2120
- detailEvents.push(event("Parsed", `phase:${String(args.phase ?? "?")}:${String(args.status ?? "?")}`));
2495
+ detailEvents.push(
2496
+ event(
2497
+ "Parsed",
2498
+ `phase:${String(args.phase ?? "?")}:${String(args.status ?? "?")}`
2499
+ )
2500
+ );
2121
2501
  this.options.log(
2122
2502
  `lifecycle ${String(args.phase ?? "?")}:${String(args.status ?? "?")} \u2014 ${String(args.summary ?? "")}`
2123
2503
  );
2124
2504
  this.respond(msg.id, toolText("ok"));
2125
2505
  return;
2126
2506
  }
2127
- this.options.log(`unknown dynamic tool '${name}' \u2014 acknowledged empty`);
2507
+ this.options.log(
2508
+ `unknown dynamic tool '${name}' \u2014 acknowledged empty`
2509
+ );
2128
2510
  this.respond(msg.id, toolText("unsupported tool"));
2129
2511
  return;
2130
2512
  }
2131
2513
  if (method.endsWith("requestApproval") || method.includes("elicitation")) {
2132
- detailEvents.push({ ...event("Approval", method), approvalState: "denied" });
2514
+ detailEvents.push({
2515
+ ...event("Approval", method),
2516
+ approvalState: "denied"
2517
+ });
2133
2518
  this.options.log(
2134
2519
  `approval requested (${method}) under approvalPolicy=never \u2014 DENIED (unattended run)`
2135
2520
  );
@@ -2148,6 +2533,7 @@ var CodexAppServer = class {
2148
2533
  developerInstructions: prompt,
2149
2534
  dynamicTools: dynamicToolDefinitions()
2150
2535
  });
2536
+ usage = mergeCodexThreadStartUsage(started, usage);
2151
2537
  threadId = String(
2152
2538
  started.threadId ?? started.thread?.id ?? ""
2153
2539
  );
@@ -2200,11 +2586,14 @@ var CodexAppServer = class {
2200
2586
  timeout(this.options.resumeTurnTimeoutMs)
2201
2587
  ]);
2202
2588
  } catch (error) {
2203
- return { status: "crashed", reason: `thread resume failed: ${String(error)}` };
2589
+ return {
2590
+ status: "crashed",
2591
+ reason: `thread resume failed: ${String(error)}`
2592
+ };
2204
2593
  }
2205
2594
  }
2206
2595
  if (outcome === "timeout")
2207
- return packet ? { status: "completed", packet, lastAgentMessage } : { status: "timeout" };
2596
+ return packet ? { status: "completed", packet, lastAgentMessage, fileChangeCount } : { status: "timeout" };
2208
2597
  if (outcome !== "completed")
2209
2598
  return { status: "crashed", reason: String(outcome) };
2210
2599
  if (!packet && threadId) {
@@ -2222,14 +2611,22 @@ var CodexAppServer = class {
2222
2611
  } catch {
2223
2612
  }
2224
2613
  }
2225
- await this.emitTurnTelemetry(telemetry, event, detailEvents, usage, packet).catch(
2614
+ await this.emitTurnTelemetry(
2615
+ telemetry,
2616
+ event,
2617
+ detailEvents,
2618
+ usage,
2619
+ packet
2620
+ ).catch(
2226
2621
  (error) => this.options.log(`telemetry emit failed (non-fatal): ${String(error)}`)
2227
2622
  );
2228
- return { status: "completed", packet, lastAgentMessage };
2623
+ return { status: "completed", packet, lastAgentMessage, fileChangeCount };
2229
2624
  }
2230
2625
  async emitTurnTelemetry(context, makeEvent, details, usage, packet) {
2231
2626
  if (!context?.decompositionId) {
2232
- this.options.log("telemetry absent: claimed task carried no wlp-decomposition tag");
2627
+ this.options.log(
2628
+ "telemetry absent: task is unmanaged or legacy dispatch attribution could not be resolved"
2629
+ );
2233
2630
  return;
2234
2631
  }
2235
2632
  const verdict = verdictForTelemetry(packet?.terminal_status);
@@ -2240,6 +2637,9 @@ var CodexAppServer = class {
2240
2637
  contextUsed: usage?.contextUsed ?? null,
2241
2638
  contextWindow: usage?.contextWindow ?? null,
2242
2639
  modelId: usage?.modelId ?? this.options.model ?? null,
2640
+ cachedInputTokens: usage?.cachedInputTokens ?? null,
2641
+ reasoningEffort: usage?.reasoningEffort ?? null,
2642
+ remainingAllowance: usage?.remainingAllowance ?? null,
2243
2643
  verdict
2244
2644
  };
2245
2645
  await this.options.emitTelemetry(
@@ -2258,19 +2658,47 @@ var CodexAppServer = class {
2258
2658
  this.child?.kill("SIGTERM");
2259
2659
  this.child = void 0;
2260
2660
  }
2261
- request(method, params) {
2661
+ request(method, params, timeoutMs) {
2262
2662
  const child = this.child;
2263
2663
  if (!child || this.exited)
2264
2664
  return Promise.reject(new Error("codex app-server is not running"));
2265
2665
  const id = this.nextId++;
2266
- return new Promise((resolve5, reject) => {
2267
- this.pending.set(id, { resolve: resolve5, reject });
2268
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
2666
+ return new Promise((resolve6, reject) => {
2667
+ let timer;
2668
+ const clearTimer = () => {
2669
+ if (timer) clearTimeout(timer);
2670
+ };
2671
+ const resolvePending = (value) => {
2672
+ clearTimer();
2673
+ resolve6(value);
2674
+ };
2675
+ const rejectPending = (error) => {
2676
+ clearTimer();
2677
+ reject(error);
2678
+ };
2679
+ this.pending.set(id, {
2680
+ resolve: resolvePending,
2681
+ reject: rejectPending
2682
+ });
2683
+ if (timeoutMs !== void 0) {
2684
+ timer = setTimeout(() => {
2685
+ if (!this.pending.delete(id)) return;
2686
+ rejectPending(
2687
+ new Error(
2688
+ `codex app-server ${method} timed out after ${timeoutMs}ms`
2689
+ )
2690
+ );
2691
+ }, timeoutMs);
2692
+ }
2693
+ const payload = params === void 0 ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
2694
+ child.stdin.write(JSON.stringify(payload) + "\n");
2269
2695
  });
2270
2696
  }
2271
2697
  respond(id, result) {
2272
2698
  if (id === void 0 || !this.child) return;
2273
- this.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
2699
+ this.child.stdin.write(
2700
+ JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n"
2701
+ );
2274
2702
  }
2275
2703
  onLine(line) {
2276
2704
  if (!line.trim()) return;
@@ -2295,7 +2723,8 @@ var CodexAppServer = class {
2295
2723
  this.onServerRequest?.(msg);
2296
2724
  return;
2297
2725
  }
2298
- if (msg.method && rateLimitMethod(msg.method)) this.tapRateLimits(msg.params ?? {});
2726
+ if (msg.method && rateLimitMethod(msg.method))
2727
+ this.tapRateLimits(msg.params ?? {});
2299
2728
  if (msg.method) this.onNotification?.(msg);
2300
2729
  if (msg.method && terminalMethod(msg.method)) {
2301
2730
  this.onServerRequest?.(msg);
@@ -2307,10 +2736,10 @@ var CodexAppServer = class {
2307
2736
  if (parsed) this.options.onRateLimits?.(parsed);
2308
2737
  }
2309
2738
  exitAsResult() {
2310
- return new Promise((resolve5) => {
2739
+ return new Promise((resolve6) => {
2311
2740
  this.child?.once(
2312
2741
  "exit",
2313
- (code) => resolve5(`app-server exited (${code ?? "signal"})`)
2742
+ (code) => resolve6(`app-server exited (${code ?? "signal"})`)
2314
2743
  );
2315
2744
  });
2316
2745
  }
@@ -2335,7 +2764,9 @@ function terminalMethod(method) {
2335
2764
  return method === "turn/completed" || method.endsWith("/turn/completed");
2336
2765
  }
2337
2766
  function timeout(ms) {
2338
- return new Promise((resolve5) => setTimeout(() => resolve5("timeout"), ms).unref?.());
2767
+ return new Promise(
2768
+ (resolve6) => setTimeout(() => resolve6("timeout"), ms).unref?.()
2769
+ );
2339
2770
  }
2340
2771
  function dynamicToolDefinitions() {
2341
2772
  return [
@@ -2374,12 +2805,16 @@ function dynamicToolDefinitions() {
2374
2805
  // src/executor-run/delivery.ts
2375
2806
  import { execFile as execFile2 } from "child_process";
2376
2807
  function createGitRunner(rootDir) {
2377
- return (bin, args) => new Promise((resolve5) => {
2808
+ return (bin, args) => new Promise((resolve6) => {
2378
2809
  execFile2(
2379
2810
  bin,
2380
2811
  bin === "git" ? ["-C", rootDir, ...args] : args,
2381
2812
  { cwd: rootDir, maxBuffer: 10 * 1024 * 1024 },
2382
- (error, stdout, stderr) => resolve5({ ok: !error, stdout: String(stdout), stderr: String(stderr) })
2813
+ (error, stdout, stderr) => resolve6({
2814
+ ok: !error,
2815
+ stdout: String(stdout),
2816
+ stderr: String(stderr)
2817
+ })
2383
2818
  );
2384
2819
  });
2385
2820
  }
@@ -2411,11 +2846,27 @@ async function checkRootReady(git, allowDirty) {
2411
2846
  async function deliverTurn(git, options) {
2412
2847
  const status = await git("git", ["status", "--porcelain"]);
2413
2848
  if (!status.ok)
2414
- return { delivered: false, note: `delivery skipped \u2014 git status failed: ${status.stderr.trim()}` };
2849
+ return {
2850
+ delivered: false,
2851
+ note: `delivery skipped \u2014 git status failed: ${status.stderr.trim()}`
2852
+ };
2415
2853
  const preDirty = new Set(options.snapshot.dirtyPaths);
2416
- const turnPaths = porcelainPaths(status.stdout).filter((p) => !preDirty.has(p));
2417
- if (turnPaths.length === 0)
2418
- return { delivered: false, note: "no file changes produced by the turn \u2014 nothing to deliver" };
2854
+ const turnPaths = porcelainPaths(status.stdout).filter(
2855
+ (p) => !preDirty.has(p)
2856
+ );
2857
+ if (turnPaths.length === 0) {
2858
+ if ((options.turnFileChangeCount ?? 0) > 0) {
2859
+ const toplevel = await git("git", ["rev-parse", "--show-toplevel"]);
2860
+ const where = toplevel.ok ? toplevel.stdout.trim() : "(rev-parse failed)";
2861
+ const diagnosis = `WRITE-VOID (FR-496 facet 4): turn emitted ${options.turnFileChangeCount} fileChange item(s) but the tree at ${where} is clean \u2014 the turn's writes landed somewhere else. Verify the executor's --root, the fleet entry root, and the codex thread cwd.`;
2862
+ options.log(diagnosis);
2863
+ return { delivered: false, note: diagnosis };
2864
+ }
2865
+ return {
2866
+ delivered: false,
2867
+ note: "no file changes produced by the turn \u2014 nothing to deliver"
2868
+ };
2869
+ }
2419
2870
  const branch = await freeBranchName(git, `task/${slug(options.taskId)}`);
2420
2871
  const created = await git("git", ["checkout", "-b", branch]);
2421
2872
  if (!created.ok)
@@ -2432,11 +2883,14 @@ async function deliverTurn(git, options) {
2432
2883
  "-m",
2433
2884
  commitMessage(options)
2434
2885
  ]);
2435
- if (!committed.ok) return failBack(`git commit failed: ${committed.stderr.trim()}`);
2886
+ if (!committed.ok)
2887
+ return failBack(`git commit failed: ${committed.stderr.trim()}`);
2436
2888
  const sha = (await git("git", ["rev-parse", "--short", "HEAD"])).stdout.trim();
2437
2889
  const pushed = await git("git", ["push", "-u", "origin", branch]);
2438
2890
  if (!pushed.ok)
2439
- notes.push(`push failed (${firstLine(pushed.stderr)}) \u2014 branch is local-only`);
2891
+ notes.push(
2892
+ `push failed (${firstLine(pushed.stderr)}) \u2014 branch is local-only`
2893
+ );
2440
2894
  let prUrl;
2441
2895
  if (options.raisePr && pushed.ok) {
2442
2896
  const pr = await git("gh", [
@@ -2450,7 +2904,10 @@ async function deliverTurn(git, options) {
2450
2904
  prBody(options, sha)
2451
2905
  ]);
2452
2906
  if (pr.ok) prUrl = firstLine(pr.stdout);
2453
- else notes.push(`PR raise failed (${firstLine(pr.stderr)}) \u2014 raise manually from ${branch}`);
2907
+ else
2908
+ notes.push(
2909
+ `PR raise failed (${firstLine(pr.stderr)}) \u2014 raise manually from ${branch}`
2910
+ );
2454
2911
  }
2455
2912
  notes.unshift(
2456
2913
  `delivered ${turnPaths.length} path(s) to ${branch} @ ${sha}${prUrl ? ` \u2014 PR ${prUrl}` : ""}`
@@ -2498,85 +2955,6 @@ function firstLine(text2) {
2498
2955
  return text2.trim().split("\n")[0] ?? "";
2499
2956
  }
2500
2957
 
2501
- // src/executor-run/request.ts
2502
- var AuthExpiredError = class extends Error {
2503
- constructor(message) {
2504
- super(message);
2505
- this.name = "AuthExpiredError";
2506
- }
2507
- };
2508
- var HttpError = class extends Error {
2509
- constructor(status, method, path, body) {
2510
- super(`${method} ${path} failed (${status}): ${body}`);
2511
- this.status = status;
2512
- this.method = method;
2513
- this.path = path;
2514
- this.body = body;
2515
- this.name = "HttpError";
2516
- }
2517
- status;
2518
- method;
2519
- path;
2520
- body;
2521
- };
2522
- function createAuthedRequest(cfg, deps = {}) {
2523
- const getToken = deps.getToken ?? requireToken;
2524
- const refresh = deps.refreshToken ?? forceRefreshToken;
2525
- const doFetch = deps.fetch ?? fetch;
2526
- const call = async (path, init, token) => doFetch(`${cfg.baseUrl}${path}`, {
2527
- ...init,
2528
- headers: {
2529
- authorization: `Bearer ${token}`,
2530
- tenant: cfg.tenant,
2531
- "content-type": "application/json",
2532
- "x-sechroom-surface": "cli",
2533
- ...init?.headers
2534
- }
2535
- });
2536
- return async (path, init) => {
2537
- const method = init?.method ?? "GET";
2538
- let token;
2539
- try {
2540
- token = await getToken(cfg);
2541
- } catch (error) {
2542
- throw new AuthExpiredError(
2543
- error instanceof Error ? error.message : String(error)
2544
- );
2545
- }
2546
- let response = await call(path, init, token);
2547
- if (response.status === 401) {
2548
- let fresh;
2549
- try {
2550
- fresh = await refresh(cfg);
2551
- } catch (error) {
2552
- throw new AuthExpiredError(
2553
- error instanceof Error ? error.message : String(error)
2554
- );
2555
- }
2556
- response = await call(path, init, fresh);
2557
- if (response.status === 401)
2558
- throw new AuthExpiredError(
2559
- `${method} ${path} still 401 after token refresh \u2014 re-authenticate (\`sechroom login\`).`
2560
- );
2561
- }
2562
- if (!response.ok)
2563
- throw new HttpError(
2564
- response.status,
2565
- method,
2566
- path,
2567
- await safeText(response)
2568
- );
2569
- return await response.json();
2570
- };
2571
- }
2572
- async function safeText(response) {
2573
- try {
2574
- return await response.text();
2575
- } catch {
2576
- return "";
2577
- }
2578
- }
2579
-
2580
2958
  // src/executor-run/driver.ts
2581
2959
  function verdictFor(terminalStatus) {
2582
2960
  switch (terminalStatus) {
@@ -2591,92 +2969,150 @@ function verdictFor(terminalStatus) {
2591
2969
  return "soft-fail";
2592
2970
  }
2593
2971
  }
2972
+ var DEFAULT_TRANSIENT_BACKOFF = {
2973
+ baseMs: 2e3,
2974
+ maxMs: 6e4,
2975
+ maxConsecutive: 20
2976
+ };
2977
+ var BackoffCeilingExhaustedError = class extends Error {
2978
+ constructor(attempts, lastError) {
2979
+ super(
2980
+ `driven executor exiting: ${attempts} consecutive API interruptions exhausted the backoff ceiling \u2014 last error: ${String(lastError)}`
2981
+ );
2982
+ this.attempts = attempts;
2983
+ this.lastError = lastError;
2984
+ this.name = "BackoffCeilingExhaustedError";
2985
+ }
2986
+ attempts;
2987
+ lastError;
2988
+ };
2594
2989
  async function runDriverLoop(ports, options) {
2595
- const summary = { processed: 0, completed: 0, abandoned: 0 };
2990
+ const summary = {
2991
+ processed: 0,
2992
+ completed: 0,
2993
+ abandoned: 0
2994
+ };
2995
+ const backoff = { ...DEFAULT_TRANSIENT_BACKOFF, ...options.transientBackoff };
2596
2996
  let admissionDeferred = false;
2997
+ let consecutiveTransient = 0;
2597
2998
  while (!options.stopping()) {
2598
- if (ports.checkAdmission) {
2599
- const admission = await ports.checkAdmission();
2600
- if (!admission.ok) {
2601
- admissionDeferred = true;
2999
+ try {
3000
+ if (ports.checkAdmission) {
3001
+ const admission = await ports.checkAdmission();
3002
+ if (!admission.ok) {
3003
+ admissionDeferred = true;
3004
+ ports.log(
3005
+ `ADMISSION DEFERRED \u2014 not claiming: ${admission.reason ?? "usage budget exhausted"}`
3006
+ );
3007
+ await ports.waitForWake(options.pollMs);
3008
+ continue;
3009
+ }
3010
+ if (admissionDeferred) {
3011
+ admissionDeferred = false;
3012
+ ports.log("admission recovered \u2014 resuming claims");
3013
+ }
3014
+ }
3015
+ if (ports.checkRootReady) {
3016
+ const ready = await ports.checkRootReady();
3017
+ if (!ready.ok) {
3018
+ ports.log(
3019
+ `root not ready \u2014 not claiming: ${ready.reason ?? "unknown"}`
3020
+ );
3021
+ await ports.waitForWake(options.pollMs);
3022
+ continue;
3023
+ }
3024
+ }
3025
+ const claim = await ports.claimNext();
3026
+ if (consecutiveTransient > 0) {
2602
3027
  ports.log(
2603
- `ADMISSION DEFERRED \u2014 not claiming: ${admission.reason ?? "usage budget exhausted"}`
3028
+ `API reachable again after ${consecutiveTransient} interruption(s) \u2014 resuming`
2604
3029
  );
2605
- await ports.waitForWake(options.pollMs);
2606
- continue;
2607
- }
2608
- if (admissionDeferred) {
2609
- admissionDeferred = false;
2610
- ports.log("admission recovered \u2014 resuming claims");
3030
+ consecutiveTransient = 0;
2611
3031
  }
2612
- }
2613
- if (ports.checkRootReady) {
2614
- const ready = await ports.checkRootReady();
2615
- if (!ready.ok) {
2616
- ports.log(`root not ready \u2014 not claiming: ${ready.reason ?? "unknown"}`);
3032
+ if (!claim) {
2617
3033
  await ports.waitForWake(options.pollMs);
2618
3034
  continue;
2619
3035
  }
2620
- }
2621
- const claim = await ports.claimNext();
2622
- if (!claim) {
2623
- await ports.waitForWake(options.pollMs);
2624
- continue;
2625
- }
2626
- summary.processed++;
2627
- ports.log(`claimed ${claim.memoryId} (lease ${claim.leaseId})`);
2628
- const task = await ports.loadTask(claim.memoryId);
2629
- const stopHeartbeat = ports.startLeaseHeartbeat(claim);
2630
- let result;
2631
- try {
2632
- result = await ports.runTurn(task, claim);
2633
- } catch (e) {
2634
- if (e instanceof AuthExpiredError) throw e;
2635
- result = { status: "crashed", reason: String(e) };
2636
- } finally {
2637
- stopHeartbeat();
2638
- }
2639
- if (result.status === "crashed" || result.status === "timeout") {
2640
- summary.abandoned++;
2641
- ports.log(
2642
- `ABANDONED ${claim.memoryId}: ${result.status === "timeout" ? "turn timed out" : result.reason} \u2014 lease will expire and the task re-offers (work may re-run).`
2643
- );
2644
- } else {
2645
- const verdict = verdictFor(result.packet?.terminal_status);
2646
- let text2 = closeoutText(task, result);
2647
- if (ports.deliver) {
2648
- try {
2649
- const delivery = await ports.deliver(claim, task, verdict);
2650
- ports.log(`delivery: ${delivery.note}`);
2651
- text2 += `
3036
+ summary.processed++;
3037
+ ports.log(`claimed ${claim.memoryId} (lease ${claim.leaseId})`);
3038
+ const task = await ports.loadTask(claim.memoryId);
3039
+ const stopHeartbeat = ports.startLeaseHeartbeat(claim);
3040
+ let result;
3041
+ try {
3042
+ result = await ports.runTurn(task, claim);
3043
+ } catch (e) {
3044
+ if (e instanceof AuthExpiredError) throw e;
3045
+ result = { status: "crashed", reason: String(e) };
3046
+ } finally {
3047
+ stopHeartbeat();
3048
+ }
3049
+ if (result.status === "crashed" || result.status === "timeout") {
3050
+ summary.abandoned++;
3051
+ ports.log(
3052
+ `ABANDONED ${claim.memoryId}: ${result.status === "timeout" ? "turn timed out" : result.reason} \u2014 lease will expire and the task re-offers (work may re-run).`
3053
+ );
3054
+ } else {
3055
+ const verdict = verdictFor(result.packet?.terminal_status);
3056
+ let text2 = closeoutText(task, result);
3057
+ if (ports.deliver) {
3058
+ try {
3059
+ const delivery = await ports.deliver(
3060
+ claim,
3061
+ task,
3062
+ verdict,
3063
+ result.status === "completed" ? result.fileChangeCount ?? 0 : 0
3064
+ );
3065
+ ports.log(`delivery: ${delivery.note}`);
3066
+ text2 += `
2652
3067
 
2653
3068
  Delivery: ${delivery.note}`;
2654
- } catch (e) {
2655
- if (e instanceof AuthExpiredError) throw e;
2656
- ports.log(`delivery threw (continuing to completion): ${String(e)}`);
2657
- text2 += `
3069
+ } catch (e) {
3070
+ if (e instanceof AuthExpiredError) throw e;
3071
+ ports.log(
3072
+ `delivery threw (continuing to completion): ${String(e)}`
3073
+ );
3074
+ text2 += `
2658
3075
 
2659
3076
  Delivery: FAILED unexpectedly (${String(e)}) \u2014 changes remain in the executor root.`;
3077
+ }
3078
+ }
3079
+ try {
3080
+ const done = await ports.completeLease(
3081
+ claim,
3082
+ verdict,
3083
+ text2,
3084
+ `${task.title} \u2014 driven closeout`
3085
+ );
3086
+ summary.completed++;
3087
+ ports.log(
3088
+ `completed ${claim.memoryId} verdict:${verdict} \u2192 ${done.completionMemoryId ?? done.outcome}`
3089
+ );
3090
+ } catch (e) {
3091
+ if (e instanceof AuthExpiredError) throw e;
3092
+ summary.abandoned++;
3093
+ ports.log(
3094
+ `COMPLETE REJECTED for ${claim.memoryId} (${String(e)}) \u2014 task will re-offer; investigate the heartbeat gap.`
3095
+ );
2660
3096
  }
2661
3097
  }
2662
- try {
2663
- const done = await ports.completeLease(
2664
- claim,
2665
- verdict,
2666
- text2,
2667
- `${task.title} \u2014 driven closeout`
2668
- );
2669
- summary.completed++;
2670
- ports.log(
2671
- `completed ${claim.memoryId} verdict:${verdict} \u2192 ${done.completionMemoryId ?? done.outcome}`
2672
- );
2673
- } catch (e) {
2674
- if (e instanceof AuthExpiredError) throw e;
2675
- summary.abandoned++;
3098
+ } catch (e) {
3099
+ if (e instanceof AuthExpiredError) throw e;
3100
+ consecutiveTransient++;
3101
+ if (consecutiveTransient > backoff.maxConsecutive) {
2676
3102
  ports.log(
2677
- `COMPLETE REJECTED for ${claim.memoryId} (${String(e)}) \u2014 task will re-offer; investigate the heartbeat gap.`
3103
+ `FATAL: ${backoff.maxConsecutive} consecutive API interruptions exhausted the backoff ceiling \u2014 deregistering and exiting (loud). Last error: ${String(e)}`
2678
3104
  );
3105
+ throw new BackoffCeilingExhaustedError(backoff.maxConsecutive, e);
2679
3106
  }
3107
+ const delay = Math.min(
3108
+ backoff.baseMs * 2 ** (consecutiveTransient - 1),
3109
+ backoff.maxMs
3110
+ );
3111
+ ports.log(
3112
+ `API interruption (${consecutiveTransient}/${backoff.maxConsecutive}) \u2014 backing off ${delay}ms and continuing: ${String(e)}`
3113
+ );
3114
+ await ports.waitForWake(delay);
3115
+ continue;
2680
3116
  }
2681
3117
  if (options.once) break;
2682
3118
  }
@@ -2709,8 +3145,152 @@ function startLeaseHeartbeat(beat, log, intervalMs = 3e4, timers = {}) {
2709
3145
 
2710
3146
  // src/executor-run/fleet.ts
2711
3147
  import { spawn as spawn2 } from "child_process";
3148
+ import { createHash as createHash2 } from "crypto";
3149
+ import {
3150
+ closeSync,
3151
+ mkdirSync as mkdirSync6,
3152
+ openSync,
3153
+ readFileSync as readFileSync4,
3154
+ rmSync as rmSync3,
3155
+ writeFileSync as writeFileSync5
3156
+ } from "fs";
2712
3157
  import { readFile } from "fs/promises";
2713
- import { resolve } from "path";
3158
+ import {
3159
+ createConnection,
3160
+ createServer as createServer2
3161
+ } from "net";
3162
+ import { tmpdir } from "os";
3163
+ import { dirname as dirname4, join as join6, resolve } from "path";
3164
+ var DEFAULT_RESTART_POLICY = {
3165
+ enabled: true,
3166
+ maxRetries: 10,
3167
+ backoffMs: 1e3,
3168
+ maxBackoffMs: 3e4,
3169
+ resetAfterMs: 6e4
3170
+ };
3171
+ var MAX_ROUTE_LOG_ENTRIES = 1e3;
3172
+ var MAX_EARLY_UNROUTED_RESERVATIONS = 1e3;
3173
+ function createFleetChildRouteInbox(options) {
3174
+ let ready = false;
3175
+ let pending;
3176
+ let activeRoute;
3177
+ const releasedClaims = /* @__PURE__ */ new Set();
3178
+ const report = () => {
3179
+ if (!options.enabled) return;
3180
+ options.send({
3181
+ type: "fleet-child-routing-status",
3182
+ instanceKey: options.instanceKey,
3183
+ instanceId: options.instanceId,
3184
+ ...options.localStatus(),
3185
+ ready
3186
+ });
3187
+ };
3188
+ const finish = (claim) => {
3189
+ const active2 = pending;
3190
+ if (!active2) return;
3191
+ clearTimeout(active2.windowTimer);
3192
+ if (active2.reservationTimer) clearTimeout(active2.reservationTimer);
3193
+ pending = void 0;
3194
+ ready = false;
3195
+ report();
3196
+ active2.resolve(claim);
3197
+ };
3198
+ const onMessage = (raw) => {
3199
+ if (!raw || typeof raw !== "object") return;
3200
+ const message = raw;
3201
+ if (message.type === "fleet-route-release" && typeof message.reservationId === "string") {
3202
+ if (pending && message.reservationId === pending.reservationId) {
3203
+ finish(void 0);
3204
+ return;
3205
+ }
3206
+ if (activeRoute && message.reservationId === activeRoute.reservationId) {
3207
+ const reason = message.reason ?? "route released by fleet node";
3208
+ releasedClaims.add(activeRoute.leaseId);
3209
+ options.onClaimRelease?.(activeRoute.leaseId, reason);
3210
+ activeRoute = void 0;
3211
+ }
3212
+ return;
3213
+ }
3214
+ if (!pending) return;
3215
+ if (message.type === "fleet-route-reserve" && typeof message.reservationId === "string" && typeof message.generationId === "string" && ready && !pending.reservationId) {
3216
+ clearTimeout(pending.windowTimer);
3217
+ ready = false;
3218
+ pending.reservationId = message.reservationId;
3219
+ report();
3220
+ options.send({
3221
+ type: "fleet-route-reserved",
3222
+ instanceKey: options.instanceKey,
3223
+ reservationId: message.reservationId
3224
+ });
3225
+ pending.reservationTimer = setTimeout(() => {
3226
+ options.send({
3227
+ type: "fleet-route-unrouted",
3228
+ instanceKey: options.instanceKey,
3229
+ reservationId: message.reservationId,
3230
+ reason: "reservation orphaned before claim mint began"
3231
+ });
3232
+ finish(void 0);
3233
+ }, options.orphanReservationTimeoutMs ?? 1e4);
3234
+ pending.reservationTimer.unref?.();
3235
+ return;
3236
+ }
3237
+ if (message.type === "fleet-route-claiming" && typeof message.reservationId === "string" && message.reservationId === pending.reservationId) {
3238
+ if (pending.reservationTimer) clearTimeout(pending.reservationTimer);
3239
+ pending.reservationTimer = setTimeout(() => {
3240
+ options.send({
3241
+ type: "fleet-route-unrouted",
3242
+ instanceKey: options.instanceKey,
3243
+ reservationId: message.reservationId,
3244
+ reason: "claim mint window expired before handoff"
3245
+ });
3246
+ finish(void 0);
3247
+ }, options.claimMintTimeoutMs ?? 6e4);
3248
+ pending.reservationTimer.unref?.();
3249
+ return;
3250
+ }
3251
+ if (message.type === "fleet-route-claim" && typeof message.reservationId === "string" && message.reservationId === pending.reservationId && message.claim) {
3252
+ activeRoute = {
3253
+ reservationId: message.reservationId,
3254
+ leaseId: message.claim.leaseId
3255
+ };
3256
+ options.send({
3257
+ type: "fleet-route-claim-accepted",
3258
+ instanceKey: options.instanceKey,
3259
+ reservationId: message.reservationId,
3260
+ leaseId: message.claim.leaseId
3261
+ });
3262
+ finish(message.claim);
3263
+ return;
3264
+ }
3265
+ };
3266
+ if (options.enabled) options.subscribe(onMessage);
3267
+ return {
3268
+ report,
3269
+ waitForClaim() {
3270
+ if (!options.enabled) return Promise.resolve(void 0);
3271
+ if (pending)
3272
+ throw new Error("fleet child already has a pending route window");
3273
+ activeRoute = void 0;
3274
+ ready = true;
3275
+ return new Promise((resolvePromise) => {
3276
+ const windowTimer = setTimeout(
3277
+ () => finish(void 0),
3278
+ options.routeWindowMs ?? 1e3
3279
+ );
3280
+ windowTimer.unref?.();
3281
+ pending = { resolve: resolvePromise, windowTimer };
3282
+ report();
3283
+ });
3284
+ },
3285
+ consumeClaimRelease(leaseId) {
3286
+ return releasedClaims.delete(leaseId);
3287
+ },
3288
+ close() {
3289
+ finish(void 0);
3290
+ if (options.enabled) options.unsubscribe(onMessage);
3291
+ }
3292
+ };
3293
+ }
2714
3294
  async function readFleetConfig(path) {
2715
3295
  const parsed = JSON.parse(
2716
3296
  await readFile(resolve(path), "utf8")
@@ -2736,7 +3316,7 @@ async function readFleetConfig(path) {
2736
3316
  }
2737
3317
  return parsed;
2738
3318
  }
2739
- function entryArgs(entry, parentId) {
3319
+ function entryArgs(entry, parentId, activationMode) {
2740
3320
  const args = [
2741
3321
  "executor",
2742
3322
  "run",
@@ -2749,6 +3329,8 @@ function entryArgs(entry, parentId) {
2749
3329
  if (v !== void 0) args.push(flag, String(v));
2750
3330
  };
2751
3331
  value("--parent-id", parentId);
3332
+ if (parentId) args.push("--fleet-child");
3333
+ if (activationMode === "Detached") args.push("--activation-mode", "detached");
2752
3334
  value("--lane", entry.lane);
2753
3335
  value("--model", entry.model);
2754
3336
  value("--connector", entry.connector);
@@ -2761,13 +3343,31 @@ function entryArgs(entry, parentId) {
2761
3343
  value("--codex-bin", entry.codexBin);
2762
3344
  value("--sandbox", entry.sandbox);
2763
3345
  value("--usage-reserve", entry.usageReserve);
3346
+ if (entry.excludeTags?.length)
3347
+ args.push("--exclude-tag", ...entry.excludeTags);
2764
3348
  return args;
2765
3349
  }
2766
3350
  function superviseFleet(config2, options = {}) {
2767
3351
  const log = options.log ?? ((line) => process.stderr.write(`${line}
2768
3352
  `));
3353
+ const now = options.now ?? (() => Date.now());
3354
+ const scheduleRestart = options.scheduleRestart ?? ((run, ms) => {
3355
+ const timer = setTimeout(run, ms);
3356
+ timer.unref?.();
3357
+ return { cancel: () => clearTimeout(timer) };
3358
+ });
2769
3359
  const states = /* @__PURE__ */ new Map();
2770
3360
  const children = /* @__PURE__ */ new Map();
3361
+ const restartCounts = /* @__PURE__ */ new Map();
3362
+ const startedAt = /* @__PURE__ */ new Map();
3363
+ const pendingRestarts = /* @__PURE__ */ new Map();
3364
+ const routingStatus = /* @__PURE__ */ new Map();
3365
+ const routeLog = /* @__PURE__ */ new Set();
3366
+ const pendingReservations = /* @__PURE__ */ new Map();
3367
+ const pendingHandoffs = /* @__PURE__ */ new Map();
3368
+ const earlyUnroutedReservations = /* @__PURE__ */ new Map();
3369
+ const routedLeases = /* @__PURE__ */ new Map();
3370
+ const intentionalRestart = /* @__PURE__ */ new Set();
2771
3371
  let stopping = false;
2772
3372
  let resolveDone;
2773
3373
  const done = new Promise((resolvePromise) => {
@@ -2776,109 +3376,938 @@ function superviseFleet(config2, options = {}) {
2776
3376
  const status = () => log(
2777
3377
  `[fleet] ${[...states].map(([key, state]) => `${key}=${state}`).join(" ")}`
2778
3378
  );
3379
+ const policyFor = (entry) => ({
3380
+ ...DEFAULT_RESTART_POLICY,
3381
+ ...config2.restart,
3382
+ ...entry.restart
3383
+ });
3384
+ const isTerminal = (state) => state === "exited" || state === "failed";
3385
+ const maybeResolveDone = () => {
3386
+ if ([...states.values()].every(isTerminal)) resolveDone();
3387
+ };
2779
3388
  const spawnEntry = options.spawnEntry ?? ((entry, args) => {
2780
3389
  const script = process.argv[1];
2781
3390
  if (!script) throw new Error("cannot locate the sechroom CLI entrypoint");
2782
3391
  return spawn2(process.execPath, [script, ...args], {
2783
3392
  cwd: resolve(entry.root),
2784
- stdio: ["ignore", "pipe", "pipe"],
3393
+ stdio: options.parentId ? ["ignore", "pipe", "pipe", "ipc"] : ["ignore", "pipe", "pipe"],
2785
3394
  env: process.env
2786
3395
  });
2787
3396
  });
2788
- for (const entry of config2.instances) {
2789
- const child = spawnEntry(entry, entryArgs(entry, options.parentId));
2790
- children.set(entry.instanceKey, child);
2791
- states.set(entry.instanceKey, "live");
3397
+ const isChildMessage = (message) => {
3398
+ if (!message || typeof message !== "object") return false;
3399
+ const value = message;
3400
+ if (value.type === "fleet-route-reserved" && typeof value.instanceKey === "string" && typeof value.reservationId === "string")
3401
+ return true;
3402
+ if (value.type === "fleet-route-claim-accepted" && typeof value.instanceKey === "string" && typeof value.reservationId === "string" && typeof value.leaseId === "string")
3403
+ return true;
3404
+ if (value.type === "fleet-route-unrouted" && typeof value.instanceKey === "string" && typeof value.reservationId === "string" && typeof value.reason === "string")
3405
+ return true;
3406
+ if (value.type !== "fleet-child-routing-status") return false;
3407
+ const admission = value.admission;
3408
+ return typeof value.instanceKey === "string" && typeof value.instanceId === "string" && typeof value.advertisementExpiresAtMs === "number" && typeof value.ready === "boolean" && !!admission && typeof admission.ok === "boolean" && (admission.reason === void 0 || typeof admission.reason === "string");
3409
+ };
3410
+ const recordMessage = (key, child, message) => {
3411
+ if (children.get(key) !== child) return;
3412
+ if (!isChildMessage(message) || message.instanceKey !== key) return;
3413
+ if (message.type === "fleet-child-routing-status") {
3414
+ routingStatus.set(key, message);
3415
+ if (message.ready) {
3416
+ for (const [leaseId, routed2] of routedLeases) {
3417
+ if (routed2.instanceKey === key && routed2.state === "running")
3418
+ routedLeases.delete(leaseId);
3419
+ }
3420
+ }
3421
+ options.onRoutingSignal?.();
3422
+ return;
3423
+ }
3424
+ if (message.type === "fleet-route-reserved") {
3425
+ const pending2 = pendingReservations.get(message.reservationId);
3426
+ if (!pending2 || pending2.instanceKey !== key) return;
3427
+ pendingReservations.delete(message.reservationId);
3428
+ pending2.resolve(true);
3429
+ return;
3430
+ }
3431
+ if (message.type === "fleet-route-unrouted") {
3432
+ let matched = false;
3433
+ const pending2 = pendingHandoffs.get(message.reservationId);
3434
+ if (pending2?.instanceKey === key) {
3435
+ pendingHandoffs.delete(message.reservationId);
3436
+ pending2.resolve("confirmed-unrouted");
3437
+ matched = true;
3438
+ }
3439
+ for (const [leaseId, routed2] of routedLeases) {
3440
+ if (routed2.instanceKey !== key || routed2.reservationId !== message.reservationId)
3441
+ continue;
3442
+ routedLeases.delete(leaseId);
3443
+ matched = true;
3444
+ routed2.releasePromise = routed2.release(
3445
+ routed2.claim,
3446
+ `fleet child confirmed route was not accepted: ${message.reason}`
3447
+ );
3448
+ }
3449
+ const senderStatus = routingStatus.get(key);
3450
+ const separator = message.reservationId.lastIndexOf(":");
3451
+ const reservationInstanceId = separator >= 0 ? message.reservationId.slice(separator + 1) : void 0;
3452
+ if (!matched && senderStatus && reservationInstanceId === senderStatus.instanceId) {
3453
+ if (earlyUnroutedReservations.size >= MAX_EARLY_UNROUTED_RESERVATIONS) {
3454
+ const oldest = earlyUnroutedReservations.keys().next();
3455
+ if (!oldest.done) earlyUnroutedReservations.delete(oldest.value);
3456
+ }
3457
+ earlyUnroutedReservations.set(message.reservationId, {
3458
+ instanceKey: key,
3459
+ reason: message.reason
3460
+ });
3461
+ }
3462
+ return;
3463
+ }
3464
+ const pending = pendingHandoffs.get(message.reservationId);
3465
+ if (!pending || pending.instanceKey !== key || pending.leaseId !== message.leaseId)
3466
+ return;
3467
+ pendingHandoffs.delete(message.reservationId);
3468
+ const routed = routedLeases.get(message.leaseId);
3469
+ if (routed) routed.state = "running";
3470
+ pending.resolve("accepted");
3471
+ };
3472
+ const start = (entry) => {
3473
+ const key = entry.instanceKey;
3474
+ const child = spawnEntry(
3475
+ entry,
3476
+ entryArgs(entry, options.parentId, options.activationMode)
3477
+ );
3478
+ children.set(key, child);
3479
+ states.set(key, "live");
3480
+ startedAt.set(key, now());
2792
3481
  const prefix = (text2) => {
2793
3482
  for (const line of text2.replace(/\n$/, "").split("\n"))
2794
- log(`[${entry.instanceKey}] ${line}`);
3483
+ log(`[${key}] ${line}`);
2795
3484
  };
2796
3485
  const concrete = child;
2797
3486
  concrete.stdout?.on("data", (chunk) => prefix(String(chunk)));
2798
3487
  concrete.stderr?.on("data", (chunk) => prefix(String(chunk)));
2799
- child.on("exit", (code, signal) => {
2800
- states.set(entry.instanceKey, "exited");
2801
- log(`[${entry.instanceKey}] exited (${signal ?? code ?? "unknown"})`);
3488
+ child.on("message", (message) => recordMessage(key, child, message));
3489
+ child.on("exit", (code, signal) => handleExit(entry, code, signal));
3490
+ };
3491
+ const handleExit = (entry, code, signal) => {
3492
+ const key = entry.instanceKey;
3493
+ const reason = signal ?? code ?? "unknown";
3494
+ routingStatus.delete(key);
3495
+ for (const [reservationId, pending] of pendingReservations) {
3496
+ if (pending.instanceKey !== key) continue;
3497
+ pendingReservations.delete(reservationId);
3498
+ pending.resolve(false);
3499
+ }
3500
+ for (const [reservationId, pending] of pendingHandoffs) {
3501
+ if (pending.instanceKey !== key) continue;
3502
+ pendingHandoffs.delete(reservationId);
3503
+ pending.resolve("child-exited");
3504
+ }
3505
+ for (const [reservationId, unrouted] of earlyUnroutedReservations) {
3506
+ if (unrouted.instanceKey === key)
3507
+ earlyUnroutedReservations.delete(reservationId);
3508
+ }
3509
+ for (const [leaseId, routed] of routedLeases) {
3510
+ if (routed.instanceKey !== key) continue;
3511
+ routedLeases.delete(leaseId);
3512
+ routed.releasePromise = routed.release(
3513
+ routed.claim,
3514
+ "fleet child exited while holding routed claim"
3515
+ );
3516
+ }
3517
+ options.onRoutingSignal?.();
3518
+ if (intentionalRestart.has(key)) {
3519
+ intentionalRestart.delete(key);
3520
+ if (stopping) {
3521
+ states.set(key, "exited");
3522
+ log(`[${key}] exited (${reason}) \u2014 drained during restart`);
3523
+ status();
3524
+ maybeResolveDone();
3525
+ return;
3526
+ }
3527
+ restartCounts.set(key, 0);
3528
+ log(`[${key}] exited (${reason}) \u2014 restarting on request`);
3529
+ start(entry);
3530
+ status();
3531
+ return;
3532
+ }
3533
+ if (stopping || states.get(key) === "stopping") {
3534
+ states.set(key, "exited");
3535
+ log(`[${key}] exited (${reason})`);
3536
+ status();
3537
+ maybeResolveDone();
3538
+ return;
3539
+ }
3540
+ const policy = policyFor(entry);
3541
+ if (!policy.enabled) {
3542
+ states.set(key, "exited");
3543
+ log(`[${key}] exited (${reason}) \u2014 restart disabled`);
3544
+ status();
3545
+ maybeResolveDone();
3546
+ return;
3547
+ }
3548
+ const uptime = now() - (startedAt.get(key) ?? now());
3549
+ if (uptime >= policy.resetAfterMs) restartCounts.set(key, 0);
3550
+ const count = (restartCounts.get(key) ?? 0) + 1;
3551
+ if (count > policy.maxRetries) {
3552
+ states.set(key, "failed");
3553
+ log(
3554
+ `[${key}] exited (${reason}) \u2014 restart budget exhausted after ${policy.maxRetries} retries; leaving it DOWN. Restart the supervisor to revive it.`
3555
+ );
3556
+ status();
3557
+ maybeResolveDone();
3558
+ return;
3559
+ }
3560
+ restartCounts.set(key, count);
3561
+ const delay = Math.min(
3562
+ policy.backoffMs * 2 ** (count - 1),
3563
+ policy.maxBackoffMs
3564
+ );
3565
+ states.set(key, "restarting");
3566
+ log(
3567
+ `[${key}] exited (${reason}) \u2014 restart ${count}/${policy.maxRetries} in ${delay}ms`
3568
+ );
3569
+ status();
3570
+ const timer = scheduleRestart(() => {
3571
+ pendingRestarts.delete(key);
3572
+ if (stopping) {
3573
+ states.set(key, "exited");
3574
+ maybeResolveDone();
3575
+ return;
3576
+ }
3577
+ log(`[${key}] restarting (attempt ${count})`);
3578
+ start(entry);
3579
+ status();
3580
+ }, delay);
3581
+ pendingRestarts.set(key, timer);
3582
+ };
3583
+ const snapshot = () => [...states].map(([instanceKey, state]) => ({
3584
+ instanceKey,
3585
+ state,
3586
+ pid: state === "live" ? children.get(instanceKey)?.pid ?? null : null,
3587
+ restarts: restartCounts.get(instanceKey) ?? 0
3588
+ }));
3589
+ const restartChild = (instanceKey) => {
3590
+ const entry = config2.instances.find((e) => e.instanceKey === instanceKey);
3591
+ if (!entry || !states.has(instanceKey))
3592
+ return { ok: false, error: `unknown instanceKey '${instanceKey}'` };
3593
+ if (stopping) return { ok: false, error: "supervisor is shutting down" };
3594
+ if (intentionalRestart.has(instanceKey))
3595
+ return { ok: true, message: "restart already in progress" };
3596
+ const pending = pendingRestarts.get(instanceKey);
3597
+ if (pending) {
3598
+ pending.cancel();
3599
+ pendingRestarts.delete(instanceKey);
3600
+ }
3601
+ if (states.get(instanceKey) === "live") {
3602
+ intentionalRestart.add(instanceKey);
3603
+ states.set(instanceKey, "restarting");
3604
+ children.get(instanceKey)?.kill("SIGINT");
2802
3605
  status();
2803
- if ([...states.values()].every((state) => state === "exited"))
2804
- resolveDone();
3606
+ return { ok: true, message: "signalled for graceful restart" };
3607
+ }
3608
+ restartCounts.set(instanceKey, 0);
3609
+ start(entry);
3610
+ status();
3611
+ return { ok: true, message: "revived" };
3612
+ };
3613
+ const logRouteOnce = (offer, outcome, line) => {
3614
+ const key = `${offer.generationId}:${outcome}:${line}`;
3615
+ if (routeLog.has(key)) return;
3616
+ if (routeLog.size >= MAX_ROUTE_LOG_ENTRIES) routeLog.clear();
3617
+ routeLog.add(key);
3618
+ log(`[router] ${line}`);
3619
+ };
3620
+ const sendToChild = (child, message) => {
3621
+ if (!child.send) return Promise.resolve(false);
3622
+ return new Promise((resolvePromise) => {
3623
+ try {
3624
+ child.send(message, (error) => resolvePromise(!error));
3625
+ } catch {
3626
+ resolvePromise(false);
3627
+ }
2805
3628
  });
2806
- }
3629
+ };
3630
+ const releaseReservation = (child, reservationId, reason) => {
3631
+ if (child)
3632
+ void sendToChild(child, {
3633
+ type: "fleet-route-release",
3634
+ reservationId,
3635
+ reason
3636
+ });
3637
+ };
3638
+ const reserve = async (child, instanceKey, generationId, reservationId) => {
3639
+ if (!child.send) return false;
3640
+ const acknowledged = new Promise((resolvePromise) => {
3641
+ pendingReservations.set(reservationId, {
3642
+ instanceKey,
3643
+ resolve: resolvePromise
3644
+ });
3645
+ });
3646
+ const sent = await sendToChild(child, {
3647
+ type: "fleet-route-reserve",
3648
+ reservationId,
3649
+ generationId
3650
+ });
3651
+ if (!sent) {
3652
+ pendingReservations.delete(reservationId);
3653
+ return false;
3654
+ }
3655
+ const timeoutMs = options.reserveAckTimeoutMs ?? 1e3;
3656
+ let timer;
3657
+ const timedOut = new Promise((resolvePromise) => {
3658
+ timer = setTimeout(() => resolvePromise(false), timeoutMs);
3659
+ });
3660
+ let accepted;
3661
+ try {
3662
+ accepted = await Promise.race([acknowledged, timedOut]);
3663
+ } finally {
3664
+ if (timer) clearTimeout(timer);
3665
+ }
3666
+ if (!accepted) pendingReservations.delete(reservationId);
3667
+ if (!accepted) return false;
3668
+ return sendToChild(child, {
3669
+ type: "fleet-route-claiming",
3670
+ reservationId
3671
+ });
3672
+ };
3673
+ const handoff = async (child, instanceKey, reservationId, claim) => {
3674
+ if (!child.send) return "not-sent";
3675
+ let acknowledgedOutcome;
3676
+ const acknowledged = new Promise((resolvePromise) => {
3677
+ pendingHandoffs.set(reservationId, {
3678
+ instanceKey,
3679
+ leaseId: claim.leaseId,
3680
+ resolve: (outcome) => {
3681
+ acknowledgedOutcome = outcome;
3682
+ resolvePromise(outcome);
3683
+ }
3684
+ });
3685
+ });
3686
+ const sent = await sendToChild(child, {
3687
+ type: "fleet-route-claim",
3688
+ reservationId,
3689
+ claim
3690
+ });
3691
+ if (!sent) {
3692
+ return acknowledgedOutcome ?? "not-sent";
3693
+ }
3694
+ const timeoutMs = options.handoffTimeoutMs ?? 3e4;
3695
+ let timer;
3696
+ const timedOut = new Promise((resolvePromise) => {
3697
+ timer = setTimeout(() => resolvePromise("timed-out"), timeoutMs);
3698
+ });
3699
+ try {
3700
+ return await Promise.race([acknowledged, timedOut]);
3701
+ } finally {
3702
+ if (timer) clearTimeout(timer);
3703
+ }
3704
+ };
3705
+ const routeOffer = async (offer, claim, release) => {
3706
+ const suggestedKey = offer.suggestedChildInstanceKey;
3707
+ const suggestedId = offer.suggestedChildInstanceId;
3708
+ const rationale = offer.suggestedChildRationale;
3709
+ const rationaleText = routingRationaleText(rationale);
3710
+ if (!suggestedKey || !suggestedId) {
3711
+ logRouteOnce(
3712
+ offer,
3713
+ "no-suggestion",
3714
+ `${offer.memoryId}: server supplied no child suggestion (${rationaleText}); no client fallback guessed`
3715
+ );
3716
+ return "no-suggestion";
3717
+ }
3718
+ if (!options.parentId || !config2.instances.some((x) => x.instanceKey === suggestedKey)) {
3719
+ logRouteOnce(
3720
+ offer,
3721
+ "not-enrolled",
3722
+ `${offer.memoryId}: selected child ${suggestedKey} is not enrolled under this node; sibling offers remain claimable`
3723
+ );
3724
+ return "not-enrolled";
3725
+ }
3726
+ if (states.get(suggestedKey) !== "live") {
3727
+ logRouteOnce(
3728
+ offer,
3729
+ "not-live",
3730
+ `${offer.memoryId}: selected child ${suggestedKey} is not locally live; sibling offers remain claimable`
3731
+ );
3732
+ return "not-live";
3733
+ }
3734
+ if ([...routedLeases.values()].some(
3735
+ (routed) => routed.instanceKey === suggestedKey && routed.state === "handoff-pending"
3736
+ )) {
3737
+ logRouteOnce(
3738
+ offer,
3739
+ "not-ready",
3740
+ `${offer.memoryId}: selected child ${suggestedKey} still has an unresolved routed handoff; refusing another proxy claim`
3741
+ );
3742
+ return "not-ready";
3743
+ }
3744
+ const status2 = routingStatus.get(suggestedKey);
3745
+ if (!status2 || status2.instanceId !== suggestedId || status2.advertisementExpiresAtMs <= now()) {
3746
+ logRouteOnce(
3747
+ offer,
3748
+ "stale-suggestion",
3749
+ `${offer.memoryId}: selected child ${suggestedKey}/${suggestedId} is stale or no longer advertised; refusing proxy claim`
3750
+ );
3751
+ return "stale-suggestion";
3752
+ }
3753
+ if (!status2.admission.ok) {
3754
+ logRouteOnce(
3755
+ offer,
3756
+ "admission-deferred",
3757
+ `${offer.memoryId}: ADMISSION DEFERRED for selected child ${suggestedKey} \u2014 ${status2.admission.reason ?? "usage budget exhausted"}; sibling offers remain claimable`
3758
+ );
3759
+ return "admission-deferred";
3760
+ }
3761
+ if (!status2.ready) {
3762
+ logRouteOnce(
3763
+ offer,
3764
+ "not-ready",
3765
+ `${offer.memoryId}: selected child ${suggestedKey} is live but not ready for a new route; sibling offers remain claimable`
3766
+ );
3767
+ return "not-ready";
3768
+ }
3769
+ const child = children.get(suggestedKey);
3770
+ if (!child) {
3771
+ logRouteOnce(
3772
+ offer,
3773
+ "not-live",
3774
+ `${offer.memoryId}: selected child ${suggestedKey} has no live process handle; sibling offers remain claimable`
3775
+ );
3776
+ return "not-live";
3777
+ }
3778
+ const reservationId = `${offer.generationId}:${suggestedId}`;
3779
+ if (!await reserve(child, suggestedKey, offer.generationId, reservationId)) {
3780
+ releaseReservation(
3781
+ child,
3782
+ reservationId,
3783
+ "reservation acknowledgement timed out"
3784
+ );
3785
+ logRouteOnce(
3786
+ offer,
3787
+ "reservation-refused",
3788
+ `${offer.memoryId}: selected child ${suggestedKey} did not reserve the route; no proxy claim made`
3789
+ );
3790
+ return "reservation-refused";
3791
+ }
3792
+ let routedClaim;
3793
+ let earlyUnrouted;
3794
+ try {
3795
+ const reservedStatus = routingStatus.get(suggestedKey);
3796
+ if (states.get(suggestedKey) !== "live" || !reservedStatus || reservedStatus.instanceId !== suggestedId || reservedStatus.advertisementExpiresAtMs <= now()) {
3797
+ releaseReservation(child, reservationId, "stale after reservation");
3798
+ return "stale-suggestion";
3799
+ }
3800
+ try {
3801
+ routedClaim = await claim(offer, suggestedId);
3802
+ } finally {
3803
+ earlyUnrouted = earlyUnroutedReservations.get(reservationId);
3804
+ earlyUnroutedReservations.delete(reservationId);
3805
+ }
3806
+ if (!routedClaim) {
3807
+ releaseReservation(child, reservationId, "proxy claim lost");
3808
+ logRouteOnce(
3809
+ offer,
3810
+ "claim-lost",
3811
+ `${offer.memoryId}: proxy claim lost before handoff; child ${suggestedKey} released to ordinary polling`
3812
+ );
3813
+ return "claim-lost";
3814
+ }
3815
+ if (earlyUnrouted?.instanceKey === suggestedKey) {
3816
+ const released2 = await release(
3817
+ routedClaim,
3818
+ `fleet child confirmed route was not accepted before proxy claim completed: ${earlyUnrouted.reason}`
3819
+ );
3820
+ logRouteOnce(
3821
+ offer,
3822
+ "handoff-failed",
3823
+ `${offer.memoryId}: child ${suggestedKey} closed reservation ${reservationId} as unrouted before proxy claim ${routedClaim.leaseId} returned; ${released2 ? "lease released and task re-offered" : "lease release failed \u2014 operator recovery required"}`
3824
+ );
3825
+ return "handoff-failed";
3826
+ }
3827
+ if (states.get(suggestedKey) !== "live" || children.get(suggestedKey) !== child) {
3828
+ const released2 = await release(
3829
+ routedClaim,
3830
+ "fleet child exited while proxy claim was in flight"
3831
+ );
3832
+ logRouteOnce(
3833
+ offer,
3834
+ "handoff-failed",
3835
+ `${offer.memoryId}: node-held lease ${routedClaim.leaseId} has no live runner (child-exited); ${released2 ? "lease released and task re-offered" : "lease release failed \u2014 operator recovery required"}`
3836
+ );
3837
+ return "handoff-failed";
3838
+ }
3839
+ const routedLease = {
3840
+ instanceKey: suggestedKey,
3841
+ reservationId,
3842
+ claim: routedClaim,
3843
+ state: "handoff-pending",
3844
+ release
3845
+ };
3846
+ routedLeases.set(routedClaim.leaseId, routedLease);
3847
+ const handoffOutcome = await handoff(
3848
+ child,
3849
+ suggestedKey,
3850
+ reservationId,
3851
+ routedClaim
3852
+ );
3853
+ if (handoffOutcome === "accepted") {
3854
+ log(
3855
+ `[router] routed ${offer.memoryId} generation ${offer.generationId} \u2192 ${suggestedKey}/${suggestedId} via ${rationaleText} (node-held lease ${routedClaim.leaseId})`
3856
+ );
3857
+ return "routed";
3858
+ }
3859
+ if (handoffOutcome === "timed-out" || handoffOutcome === "not-sent") {
3860
+ logRouteOnce(
3861
+ offer,
3862
+ "handoff-failed",
3863
+ `${offer.memoryId}: IPC handoff acknowledgement/delivery for node-held lease ${routedClaim.leaseId} is still unknown (${handoffOutcome}); lease retained because child ${suggestedKey} may already be running it`
3864
+ );
3865
+ return "handoff-failed";
3866
+ }
3867
+ routedLeases.delete(routedClaim.leaseId);
3868
+ const released = handoffOutcome === "child-exited" || handoffOutcome === "confirmed-unrouted" ? await (routedLease.releasePromise ?? release(routedClaim, "fleet child exited during claim handoff")) : await release(routedClaim, "fleet claim handoff was not delivered");
3869
+ logRouteOnce(
3870
+ offer,
3871
+ "handoff-failed",
3872
+ `${offer.memoryId}: node-held lease ${routedClaim.leaseId} has no live runner (${handoffOutcome}); ${released ? "lease released and task re-offered" : "lease release failed \u2014 operator recovery required"}`
3873
+ );
3874
+ return "handoff-failed";
3875
+ } catch (error) {
3876
+ releaseReservation(child, reservationId, "proxy claim failed");
3877
+ throw error;
3878
+ }
3879
+ };
3880
+ for (const entry of config2.instances) start(entry);
2807
3881
  status();
2808
3882
  return {
2809
3883
  done,
3884
+ snapshot,
3885
+ restartChild,
3886
+ routeOffer,
3887
+ routingStatus,
2810
3888
  shutdown(signal = "SIGINT") {
2811
3889
  if (stopping) return done;
2812
3890
  stopping = true;
3891
+ for (const [key, timer] of pendingRestarts) {
3892
+ timer.cancel();
3893
+ if (!isTerminal(states.get(key))) states.set(key, "exited");
3894
+ }
3895
+ pendingRestarts.clear();
2813
3896
  for (const [key, child] of children) {
2814
- if (states.get(key) !== "exited") {
3897
+ if (states.get(key) === "live") {
2815
3898
  states.set(key, "stopping");
2816
3899
  child.kill(signal);
2817
3900
  }
2818
3901
  }
2819
3902
  status();
3903
+ maybeResolveDone();
2820
3904
  return done;
2821
3905
  },
2822
- states
3906
+ states,
3907
+ restartCounts
3908
+ };
3909
+ }
3910
+ function routingRationaleText(rationale) {
3911
+ if (!rationale) return "no rationale";
3912
+ return `rule ${rationale.ruleId}, reason ${rationale.reasonCode}, preference ${rationale.preference}, eligible children ${rationale.eligibleChildCount}`;
3913
+ }
3914
+ function startNodeOfferRouter(options) {
3915
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
3916
+ `));
3917
+ const pollMs = options.pollMs ?? 5e3;
3918
+ let stopping = false;
3919
+ let wake = () => {
3920
+ };
3921
+ const wait = () => new Promise((resolvePromise) => {
3922
+ const timer = setTimeout(resolvePromise, pollMs);
3923
+ timer.unref?.();
3924
+ wake = () => {
3925
+ clearTimeout(timer);
3926
+ resolvePromise();
3927
+ };
3928
+ });
3929
+ const claim = async (offer, childInstanceId) => {
3930
+ const result = await options.request(
3931
+ "/me/executor-task-proxy-claims",
3932
+ {
3933
+ method: "POST",
3934
+ body: JSON.stringify({
3935
+ generationId: offer.generationId,
3936
+ nodeInstanceId: options.nodeId,
3937
+ proxiedForInstanceId: childInstanceId
3938
+ })
3939
+ }
3940
+ );
3941
+ if (!SUCCESS.has(result.outcome) || !result.lease || result.lease.executorInstanceId !== options.nodeId || result.lease.proxiedForInstanceId !== childInstanceId) {
3942
+ log(
3943
+ `node proxy claim for ${offer.memoryId} returned ${result.outcome}${result.reason ? ` (${result.reason})` : ""}; leaving child offers to reconcile`
3944
+ );
3945
+ return void 0;
3946
+ }
3947
+ const claimed = toClaimed(result, result.tokenVersion ?? 1);
3948
+ if (!claimed) {
3949
+ log(
3950
+ `node proxy claim for ${offer.memoryId} returned ${result.outcome} without re-issued holder proof; retaining the existing lease and re-polling`
3951
+ );
3952
+ return void 0;
3953
+ }
3954
+ return {
3955
+ memoryId: claimed.memoryId,
3956
+ leaseId: claimed.leaseId,
3957
+ claimToken: claimed.claimToken,
3958
+ tokenVersion: claimed.tokenVersion,
3959
+ executorInstanceId: options.nodeId,
3960
+ decompositionId: claimed.decompositionId
3961
+ };
3962
+ };
3963
+ const releaseByTask = async (memoryId, reason) => {
3964
+ try {
3965
+ await options.request(
3966
+ `/work-executors/tasks/${encodeURIComponent(memoryId)}/force-re-offer`,
3967
+ { method: "POST", body: JSON.stringify({}) }
3968
+ );
3969
+ log(
3970
+ `released node-held claim for ${memoryId} (${reason}); task re-offered`
3971
+ );
3972
+ return true;
3973
+ } catch (error) {
3974
+ log(
3975
+ `FAILED to release node-held claim for ${memoryId} (${reason}): ${String(error)}`
3976
+ );
3977
+ return false;
3978
+ }
3979
+ };
3980
+ const release = (held, reason) => releaseByTask(held.memoryId, reason);
3981
+ const done = (async () => {
3982
+ while (!stopping) {
3983
+ try {
3984
+ const offers = await options.request(
3985
+ `/me/executor-instances/${encodeURIComponent(options.nodeId)}/dispatch-offers`
3986
+ );
3987
+ for (const offer of offers) {
3988
+ if (stopping) break;
3989
+ await options.routeOffer(offer, claim, release);
3990
+ }
3991
+ } catch (error) {
3992
+ log(`node offer reconciliation failed (${String(error)}) \u2014 retrying`);
3993
+ }
3994
+ if (!stopping) await wait();
3995
+ }
3996
+ })();
3997
+ return {
3998
+ done,
3999
+ wake: () => wake(),
4000
+ async stop() {
4001
+ stopping = true;
4002
+ wake();
4003
+ await done;
4004
+ }
4005
+ };
4006
+ }
4007
+ var DETACHED_SUPERVISOR_ENV = "SECHROOM_FLEET_DETACHED";
4008
+ function isProcessAlive(pid) {
4009
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4010
+ try {
4011
+ process.kill(pid, 0);
4012
+ return true;
4013
+ } catch (error) {
4014
+ return error.code !== "ESRCH";
4015
+ }
4016
+ }
4017
+ function readPidFile(path) {
4018
+ let raw;
4019
+ try {
4020
+ raw = readFileSync4(path, "utf8");
4021
+ } catch (error) {
4022
+ if (error.code === "ENOENT") return void 0;
4023
+ throw error;
4024
+ }
4025
+ const pid = Number.parseInt(raw.trim(), 10);
4026
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
4027
+ }
4028
+ function claimPidFile(path, options = {}) {
4029
+ const isAlive = options.isAlive ?? isProcessAlive;
4030
+ const log = options.log ?? (() => {
4031
+ });
4032
+ let raw;
4033
+ try {
4034
+ raw = readFileSync4(path, "utf8");
4035
+ } catch (error) {
4036
+ if (error.code === "ENOENT") return;
4037
+ throw error;
4038
+ }
4039
+ const pid = Number.parseInt(raw.trim(), 10);
4040
+ const valid = Number.isInteger(pid) && pid > 0;
4041
+ if (valid && isAlive(pid))
4042
+ throw new Error(
4043
+ `pid-file ${path} already names a live supervisor (pid ${pid}) \u2014 refusing to start a second one. Stop it first, or remove the pid-file if that process is not a fleet supervisor.`
4044
+ );
4045
+ log(
4046
+ `stale pid-file ${path} (${valid ? `pid ${pid}` : "unparseable"} not running) \u2014 reclaiming`
4047
+ );
4048
+ rmSync3(path, { force: true });
4049
+ }
4050
+ function writePidFile(path, pid) {
4051
+ mkdirSync6(dirname4(path), { recursive: true });
4052
+ writeFileSync5(path, `${pid}
4053
+ `, { flag: "wx" });
4054
+ }
4055
+ function removePidFileIfOwned(path, pid, log) {
4056
+ if (readPidFile(path) !== pid) return;
4057
+ try {
4058
+ rmSync3(path, { force: true });
4059
+ } catch (error) {
4060
+ log?.(`pid-file ${path} cleanup failed: ${String(error)}`);
4061
+ }
4062
+ }
4063
+ function launchDetachedSupervisor(options) {
4064
+ const log = options.log ?? (() => {
4065
+ });
4066
+ const pidFile = resolve(options.pidFile);
4067
+ const logFile = resolve(options.logFile);
4068
+ claimPidFile(pidFile, { log, isAlive: options.isAlive });
4069
+ mkdirSync6(dirname4(logFile), { recursive: true });
4070
+ const openLogFd = options.openLogFd ?? ((path) => openSync(path, "a"));
4071
+ const logFd = openLogFd(logFile);
4072
+ const spawnSupervisor = options.spawnSupervisor ?? ((fd) => {
4073
+ const script = process.argv[1];
4074
+ if (!script) throw new Error("cannot locate the sechroom CLI entrypoint");
4075
+ const argv = options.argv ?? [script, ...process.argv.slice(2)];
4076
+ return spawn2(process.execPath, argv, {
4077
+ detached: true,
4078
+ stdio: ["ignore", fd, fd],
4079
+ env: { ...process.env, [DETACHED_SUPERVISOR_ENV]: "1" }
4080
+ });
4081
+ });
4082
+ let child;
4083
+ try {
4084
+ child = spawnSupervisor(logFd);
4085
+ } finally {
4086
+ try {
4087
+ closeSync(logFd);
4088
+ } catch {
4089
+ }
4090
+ }
4091
+ const pid = child.pid;
4092
+ if (pid === void 0)
4093
+ throw new Error("detached supervisor failed to spawn (no pid)");
4094
+ try {
4095
+ writePidFile(pidFile, pid);
4096
+ } catch (error) {
4097
+ try {
4098
+ process.kill(pid, "SIGTERM");
4099
+ } catch {
4100
+ }
4101
+ throw error;
4102
+ }
4103
+ child.unref();
4104
+ return pid;
4105
+ }
4106
+ function controlSocketPath(pidFile) {
4107
+ const hash = createHash2("sha256").update(resolve(pidFile)).digest("hex").slice(0, 16);
4108
+ return join6(tmpdir(), `sechroom-fleet-${hash}.sock`);
4109
+ }
4110
+ function startControlServer(socketPath, handlers, options = {}) {
4111
+ const log = options.log ?? (() => {
4112
+ });
4113
+ rmSync3(socketPath, { force: true });
4114
+ const dispatch = (req) => {
4115
+ switch (req.command) {
4116
+ case "status":
4117
+ return { ok: true, command: "status", status: handlers.status() };
4118
+ case "restart": {
4119
+ if (!req.instanceKey)
4120
+ return { ok: false, error: "restart requires an instanceKey" };
4121
+ const r = handlers.restart(req.instanceKey);
4122
+ return r.ok ? {
4123
+ ok: true,
4124
+ command: "restart",
4125
+ message: r.message ?? "restarted"
4126
+ } : { ok: false, error: r.error ?? "restart failed" };
4127
+ }
4128
+ case "stop":
4129
+ handlers.stop();
4130
+ return { ok: true, command: "stop", message: "draining" };
4131
+ default:
4132
+ return { ok: false, error: "unknown command" };
4133
+ }
4134
+ };
4135
+ const onConnection = (socket) => {
4136
+ let buffer = "";
4137
+ const reply = (res) => {
4138
+ socket.end(`${JSON.stringify(res)}
4139
+ `);
4140
+ };
4141
+ socket.setEncoding("utf8");
4142
+ socket.on("data", (chunk) => {
4143
+ buffer += chunk;
4144
+ const nl = buffer.indexOf("\n");
4145
+ if (nl === -1) return;
4146
+ let req;
4147
+ try {
4148
+ req = JSON.parse(buffer.slice(0, nl));
4149
+ } catch {
4150
+ reply({
4151
+ ok: false,
4152
+ error: "malformed request (expected one JSON line)"
4153
+ });
4154
+ return;
4155
+ }
4156
+ try {
4157
+ reply(dispatch(req));
4158
+ } catch (error) {
4159
+ reply({ ok: false, error: String(error) });
4160
+ }
4161
+ });
4162
+ socket.on("error", () => {
4163
+ });
2823
4164
  };
4165
+ return new Promise((resolvePromise, rejectPromise) => {
4166
+ const server = createServer2(onConnection);
4167
+ server.on("error", rejectPromise);
4168
+ server.listen(socketPath, () => {
4169
+ server.removeListener("error", rejectPromise);
4170
+ server.on("error", (e) => log(`control socket error: ${String(e)}`));
4171
+ resolvePromise({
4172
+ socketPath,
4173
+ close: () => new Promise((res) => {
4174
+ server.close(() => {
4175
+ rmSync3(socketPath, { force: true });
4176
+ res();
4177
+ });
4178
+ })
4179
+ });
4180
+ });
4181
+ });
4182
+ }
4183
+ var FleetControlUnreachableError = class extends Error {
4184
+ constructor(socketPath, cause) {
4185
+ super(`no fleet supervisor is listening on ${socketPath} (${cause})`);
4186
+ this.name = "FleetControlUnreachableError";
4187
+ }
4188
+ };
4189
+ function sendControlCommand(socketPath, request, options = {}) {
4190
+ const timeoutMs = options.timeoutMs ?? 5e3;
4191
+ return new Promise((resolvePromise, rejectPromise) => {
4192
+ let settled = false;
4193
+ let buffer = "";
4194
+ const socket = createConnection(socketPath);
4195
+ const finish = (fn) => {
4196
+ if (settled) return;
4197
+ settled = true;
4198
+ socket.destroy();
4199
+ fn();
4200
+ };
4201
+ const timer = setTimeout(
4202
+ () => finish(
4203
+ () => rejectPromise(
4204
+ new Error(`fleet control request timed out after ${timeoutMs}ms`)
4205
+ )
4206
+ ),
4207
+ timeoutMs
4208
+ );
4209
+ timer.unref?.();
4210
+ socket.setEncoding("utf8");
4211
+ socket.on("connect", () => {
4212
+ socket.write(`${JSON.stringify(request)}
4213
+ `);
4214
+ });
4215
+ socket.on("data", (chunk) => {
4216
+ buffer += chunk;
4217
+ const nl = buffer.indexOf("\n");
4218
+ if (nl === -1) return;
4219
+ clearTimeout(timer);
4220
+ let res;
4221
+ try {
4222
+ res = JSON.parse(buffer.slice(0, nl));
4223
+ } catch (error) {
4224
+ finish(
4225
+ () => rejectPromise(
4226
+ new Error(`malformed control response: ${String(error)}`)
4227
+ )
4228
+ );
4229
+ return;
4230
+ }
4231
+ finish(() => resolvePromise(res));
4232
+ });
4233
+ socket.on("error", (error) => {
4234
+ clearTimeout(timer);
4235
+ const code = error.code;
4236
+ if (code === "ENOENT" || code === "ECONNREFUSED")
4237
+ finish(
4238
+ () => rejectPromise(new FleetControlUnreachableError(socketPath, code))
4239
+ );
4240
+ else finish(() => rejectPromise(error));
4241
+ });
4242
+ socket.on("close", () => {
4243
+ clearTimeout(timer);
4244
+ if (!settled)
4245
+ finish(
4246
+ () => rejectPromise(
4247
+ new FleetControlUnreachableError(socketPath, "connection closed")
4248
+ )
4249
+ );
4250
+ });
4251
+ });
2824
4252
  }
2825
4253
 
2826
4254
  // src/commands/telemetry.ts
2827
4255
  import {
2828
4256
  existsSync as existsSync7,
2829
- mkdirSync as mkdirSync7,
2830
- readFileSync as readFileSync5,
2831
- rmSync as rmSync3,
2832
- writeFileSync as writeFileSync6
4257
+ mkdirSync as mkdirSync8,
4258
+ readFileSync as readFileSync6,
4259
+ rmSync as rmSync4,
4260
+ writeFileSync as writeFileSync7
2833
4261
  } from "fs";
2834
- import { dirname as dirname6, join as join8 } from "path";
4262
+ import { homedir as homedir4 } from "os";
4263
+ import { dirname as dirname7, join as join9, parse, resolve as resolve2, sep } from "path";
2835
4264
 
2836
4265
  // src/commands/hook-install.ts
2837
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2838
- import { delimiter, dirname as dirname5, join as join7 } from "path";
4266
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4267
+ import { delimiter, dirname as dirname6, join as join8 } from "path";
2839
4268
 
2840
4269
  // src/setup/clients.ts
2841
4270
  import { existsSync as existsSync5 } from "fs";
2842
4271
  import { homedir as homedir3 } from "os";
2843
- import { dirname as dirname4, join as join6 } from "path";
4272
+ import { dirname as dirname5, join as join7 } from "path";
2844
4273
  function claudeDesktopConfigPath(home) {
2845
4274
  switch (process.platform) {
2846
4275
  case "darwin":
2847
- return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4276
+ return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2848
4277
  case "win32":
2849
- return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
4278
+ return join7(process.env.APPDATA ?? join7(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2850
4279
  default:
2851
- return join6(home, ".config", "Claude", "claude_desktop_config.json");
4280
+ return join7(home, ".config", "Claude", "claude_desktop_config.json");
2852
4281
  }
2853
4282
  }
2854
4283
  function clientTargets(cwd, opts = {}) {
2855
4284
  const home = homedir3();
2856
- const claudeDir = opts.claudeDir ?? join6(home, ".claude");
2857
- const codexHome = opts.codexHome ?? join6(home, ".codex");
4285
+ const claudeDir = opts.claudeDir ?? join7(home, ".claude");
4286
+ const codexHome = opts.codexHome ?? join7(home, ".codex");
2858
4287
  return {
2859
4288
  "claude-code": {
2860
4289
  key: "claude-code",
2861
4290
  label: "Claude Code",
2862
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
2863
- instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
4291
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".mcp.json"), format: "json" },
4292
+ instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
2864
4293
  },
2865
4294
  "claude-desktop": {
2866
4295
  key: "claude-desktop",
2867
4296
  label: "Claude Desktop",
2868
4297
  mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
2869
- instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
4298
+ instruction: { surfaceKey: "claude-desktop", path: join7(claudeDir, "CLAUDE.md") }
2870
4299
  },
2871
4300
  codex: {
2872
4301
  key: "codex",
2873
4302
  label: "Codex CLI",
2874
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
2875
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
4303
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join7(codexHome, "config.toml"), format: "toml" },
4304
+ instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
2876
4305
  },
2877
4306
  cursor: {
2878
4307
  key: "cursor",
2879
4308
  label: "Cursor",
2880
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
2881
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
4309
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".cursor", "mcp.json"), format: "json" },
4310
+ instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
2882
4311
  },
2883
4312
  antigravity: {
2884
4313
  key: "antigravity",
@@ -2889,8 +4318,8 @@ function clientTargets(cwd, opts = {}) {
2889
4318
  // `type` — comes from the `antigravity` server surface, so we don't
2890
4319
  // hardcode it here. Instructions go in the project `AGENTS.md`
2891
4320
  // (cross-tool, shared with Codex/Cursor).
2892
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
2893
- instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
4321
+ mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join7(home, ".gemini", "config", "mcp_config.json"), format: "json" },
4322
+ instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
2894
4323
  }
2895
4324
  };
2896
4325
  }
@@ -2900,10 +4329,10 @@ function detectInstalledClients(cwd) {
2900
4329
  const home = homedir3();
2901
4330
  const detected = [];
2902
4331
  if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
2903
- if (existsSync5(dirname4(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
4332
+ if (existsSync5(dirname5(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
2904
4333
  if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
2905
- if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
2906
- if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
4334
+ if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor"))) detected.push("cursor");
4335
+ if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
2907
4336
  return detected;
2908
4337
  }
2909
4338
 
@@ -2947,28 +4376,28 @@ function mergeHooks(config2, commands) {
2947
4376
  }
2948
4377
  function readJsonConfig2(path) {
2949
4378
  if (!existsSync6(path)) return {};
2950
- const raw = readFileSync4(path, "utf8");
4379
+ const raw = readFileSync5(path, "utf8");
2951
4380
  if (!raw.trim()) return {};
2952
4381
  return JSON.parse(raw);
2953
4382
  }
2954
4383
  function installHooksJson(path, commands, dryRun) {
2955
- const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
4384
+ const existed = existsSync6(path) && readFileSync5(path, "utf8").trim().length > 0;
2956
4385
  const config2 = readJsonConfig2(path);
2957
4386
  const added = mergeHooks(config2, commands);
2958
4387
  if (added === 0 && existed) return { path, status: "current" };
2959
4388
  if (!dryRun) {
2960
- mkdirSync6(dirname5(path), { recursive: true });
2961
- writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
4389
+ mkdirSync7(dirname6(path), { recursive: true });
4390
+ writeFileSync6(path, JSON.stringify(config2, null, 2) + "\n");
2962
4391
  }
2963
4392
  return { path, status: existed ? "merged" : "created" };
2964
4393
  }
2965
4394
  function installClaudeCommands(claudeDir, commands, dryRun) {
2966
- return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
4395
+ return installHooksJson(join8(claudeDir, "settings.json"), commands, dryRun);
2967
4396
  }
2968
4397
  function installCodexCommands(codexHome, commands, dryRun) {
2969
4398
  return [
2970
- installHooksJson(join7(codexHome, "hooks.json"), commands, dryRun),
2971
- installCodexFeatureFlag(join7(codexHome, "config.toml"), dryRun)
4399
+ installHooksJson(join8(codexHome, "hooks.json"), commands, dryRun),
4400
+ installCodexFeatureFlag(join8(codexHome, "config.toml"), dryRun)
2972
4401
  ];
2973
4402
  }
2974
4403
  function ensureCodexFeaturesHooks(content) {
@@ -2993,12 +4422,12 @@ function ensureCodexFeaturesHooks(content) {
2993
4422
  }
2994
4423
  function installCodexFeatureFlag(path, dryRun) {
2995
4424
  const existed = existsSync6(path);
2996
- const content = existed ? readFileSync4(path, "utf8") : "";
4425
+ const content = existed ? readFileSync5(path, "utf8") : "";
2997
4426
  const { next, changed } = ensureCodexFeaturesHooks(content);
2998
4427
  if (!changed) return { path, status: "current" };
2999
4428
  if (!dryRun) {
3000
- mkdirSync6(dirname5(path), { recursive: true });
3001
- writeFileSync5(path, next);
4429
+ mkdirSync7(dirname6(path), { recursive: true });
4430
+ writeFileSync6(path, next);
3002
4431
  }
3003
4432
  return { path, status: existed ? "merged" : "created" };
3004
4433
  }
@@ -3023,11 +4452,11 @@ function installHookSurfaces(surfaces, opts) {
3023
4452
  const out = [];
3024
4453
  for (const surface of surfaces) {
3025
4454
  if (surface === "claude") {
3026
- const path = join7(opts.claudeDir, "settings.json");
4455
+ const path = join8(opts.claudeDir, "settings.json");
3027
4456
  out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
3028
4457
  } else {
3029
- const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
3030
- const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
4458
+ const hooksJson = installHooksJson(join8(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
4459
+ const featureFlag = installCodexFeatureFlag(join8(opts.codexHome, "config.toml"), opts.dryRun);
3031
4460
  out.push({ surface, results: [hooksJson, featureFlag] });
3032
4461
  }
3033
4462
  }
@@ -3047,7 +4476,7 @@ function isSechroomOnPath() {
3047
4476
  for (const dir of pathEnv.split(delimiter)) {
3048
4477
  if (!dir) continue;
3049
4478
  for (const name of names) {
3050
- if (existsSync6(join7(dir, name))) return true;
4479
+ if (existsSync6(join8(dir, name))) return true;
3051
4480
  }
3052
4481
  }
3053
4482
  return false;
@@ -3147,14 +4576,14 @@ function registerTelemetry(program2) {
3147
4576
  "Decomposition id this session executes"
3148
4577
  ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
3149
4578
  const json = Boolean(cmd.optsWithGlobals().json);
3150
- const dir = join8(process.cwd(), ".sechroom");
3151
- mkdirSync7(dir, { recursive: true });
3152
- const path = join8(dir, BINDING_FILE);
4579
+ const dir = join9(process.cwd(), ".sechroom");
4580
+ mkdirSync8(dir, { recursive: true });
4581
+ const path = join9(dir, BINDING_FILE);
3153
4582
  const binding = {
3154
4583
  decompositionId: opts.decomposition,
3155
4584
  taskId: opts.task
3156
4585
  };
3157
- writeFileSync6(path, JSON.stringify(binding, null, 2) + "\n");
4586
+ writeFileSync7(path, JSON.stringify(binding, null, 2) + "\n");
3158
4587
  ensureStateDirIgnored(process.cwd());
3159
4588
  if (json) {
3160
4589
  emit({ bound: true, ...binding, path }, true);
@@ -3169,9 +4598,9 @@ function registerTelemetry(program2) {
3169
4598
  });
3170
4599
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
3171
4600
  const json = Boolean(cmd.optsWithGlobals().json);
3172
- const path = join8(process.cwd(), ".sechroom", BINDING_FILE);
4601
+ const path = join9(process.cwd(), ".sechroom", BINDING_FILE);
3173
4602
  const existed = existsSync7(path);
3174
- if (existed) rmSync3(path);
4603
+ if (existed) rmSync4(path);
3175
4604
  if (json) emit({ unbound: existed, path }, true);
3176
4605
  else
3177
4606
  process.stdout.write(
@@ -3188,7 +4617,16 @@ function registerTelemetry(program2) {
3188
4617
  const binding = findBinding(cwd);
3189
4618
  if (!binding) return process.exit(0);
3190
4619
  const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
3191
- const events = buildHookEvents(input, usage, binding.taskId);
4620
+ const configRoot = resolveClaudeConfigRoot(
4621
+ process.env.CLAUDE_CONFIG_DIR,
4622
+ input.transcript_path
4623
+ );
4624
+ const events = buildHookEvents(
4625
+ input,
4626
+ usage,
4627
+ binding.taskId,
4628
+ configRoot
4629
+ );
3192
4630
  if (events.length === 0) return process.exit(0);
3193
4631
  const cfg = resolveConfig(cmd.optsWithGlobals());
3194
4632
  await postTelemetry(cfg, binding.decompositionId, events);
@@ -3278,11 +4716,11 @@ async function postTelemetry(cfg, decompositionId, events) {
3278
4716
  function findBinding(start) {
3279
4717
  let dir = start;
3280
4718
  for (; ; ) {
3281
- const path = join8(dir, ".sechroom", BINDING_FILE);
4719
+ const path = join9(dir, ".sechroom", BINDING_FILE);
3282
4720
  if (existsSync7(path)) {
3283
4721
  try {
3284
4722
  const b = JSON.parse(
3285
- readFileSync5(path, "utf8")
4723
+ readFileSync6(path, "utf8")
3286
4724
  );
3287
4725
  if (b.decompositionId && b.taskId)
3288
4726
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -3290,7 +4728,7 @@ function findBinding(start) {
3290
4728
  }
3291
4729
  return null;
3292
4730
  }
3293
- const parent = dirname6(dir);
4731
+ const parent = dirname7(dir);
3294
4732
  if (parent === dir) return null;
3295
4733
  dir = parent;
3296
4734
  }
@@ -3301,7 +4739,7 @@ function parseTranscript(path) {
3301
4739
  let tokensOut = 0;
3302
4740
  let contextUsed = 0;
3303
4741
  let model = "";
3304
- for (const line of readFileSync5(path, "utf8").split("\n")) {
4742
+ for (const line of readFileSync6(path, "utf8").split("\n")) {
3305
4743
  if (!line.trim()) continue;
3306
4744
  let obj;
3307
4745
  try {
@@ -3325,7 +4763,7 @@ function windowFor(model, contextUsed = 0) {
3325
4763
  if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
3326
4764
  return contextUsed > 2e5 ? 1e6 : 2e5;
3327
4765
  }
3328
- function buildHookEvents(input, usage, taskId) {
4766
+ function buildHookEvents(input, usage, taskId, configRoot = null) {
3329
4767
  const events = [];
3330
4768
  const base = (kind, over) => ({
3331
4769
  taskId,
@@ -3338,6 +4776,7 @@ function buildHookEvents(input, usage, taskId) {
3338
4776
  approvalState: null,
3339
4777
  verdict: null,
3340
4778
  modelId: null,
4779
+ configRoot,
3341
4780
  ...over
3342
4781
  });
3343
4782
  if (usage) {
@@ -3371,6 +4810,29 @@ function buildHookEvents(input, usage, taskId) {
3371
4810
  }
3372
4811
  return events;
3373
4812
  }
4813
+ function resolveClaudeConfigRoot(configuredRoot, transcriptPath) {
4814
+ if (configuredRoot?.trim()) {
4815
+ const configured = configuredRoot.split(",").map((candidate) => candidate.trim()).filter(Boolean);
4816
+ if (configured.length === 1)
4817
+ return normalizeClaudeConfigRoot(configured.at(0));
4818
+ }
4819
+ if (!transcriptPath?.trim()) return null;
4820
+ const path = normalizeClaudeConfigRoot(transcriptPath);
4821
+ if (!path) return null;
4822
+ const projectsMarker = `${sep}projects${sep}`;
4823
+ const markerIndex = path.lastIndexOf(projectsMarker);
4824
+ return markerIndex > 0 ? normalizeClaudeConfigRoot(path.slice(0, markerIndex)) : null;
4825
+ }
4826
+ function normalizeClaudeConfigRoot(candidate) {
4827
+ const trimmed = candidate?.trim();
4828
+ if (!trimmed) return null;
4829
+ const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith(`~${sep}`) ? join9(homedir4(), trimmed.slice(2)) : trimmed;
4830
+ let normalized = resolve2(expanded);
4831
+ const rootLength = parse(normalized).root.length;
4832
+ while (normalized.length > rootLength && normalized.endsWith(sep))
4833
+ normalized = normalized.slice(0, -1);
4834
+ return normalized;
4835
+ }
3374
4836
  function isPermissionNotification(input) {
3375
4837
  const t = (input.notification_type ?? input.type ?? "").toLowerCase();
3376
4838
  if (t) return t.includes("permission");
@@ -3412,17 +4874,53 @@ function parseIntOpt(v) {
3412
4874
 
3413
4875
  // src/commands/executor-run.ts
3414
4876
  function registerExecutorRunCommand(executor) {
3415
- executor.command("fleet").description(
4877
+ const fleetCommand = executor.command("fleet").description(
3416
4878
  "Run multiple isolated driven codex executors from one config file"
3417
- ).requiredOption("--config <file>", "JSON fleet config").action(async (opts, cmd) => {
3418
- const config2 = await readFleetConfig(String(opts.config));
3419
- const cfg = resolveConfig(cmd.optsWithGlobals());
4879
+ ).option("--config <file>", "JSON fleet config").option(
4880
+ "--detach",
4881
+ "Run the supervisor as a detached service that outlives the launching shell (canonical default)",
4882
+ false
4883
+ ).option(
4884
+ "--foreground",
4885
+ "Run session-attached (the pre-service behaviour): the supervisor dies with the launching shell",
4886
+ false
4887
+ ).option(
4888
+ "--log-file <file>",
4889
+ "Detached mode: append the supervisor + prefixed child logs here (default .sechroom/fleet.log)"
4890
+ ).option(
4891
+ "--pid-file <file>",
4892
+ "Detached mode: write the supervisor pid here atomically, for adoption by the lifecycle verbs (default .sechroom/fleet.pid)"
4893
+ ).action(async (opts, cmd) => {
3420
4894
  const log = (line) => process.stderr.write(style.dim(`[fleet] ${line}
3421
4895
  `));
4896
+ const isDetachedChild = process.env.SECHROOM_FLEET_DETACHED === "1";
4897
+ if (!opts.config) fail("required option '--config <file>' not specified");
4898
+ const foreground = Boolean(opts.foreground);
4899
+ if (foreground && opts.detach)
4900
+ fail("--detach and --foreground are mutually exclusive");
4901
+ const detach = !foreground;
4902
+ const pidFile = resolve3(
4903
+ opts.pidFile ? String(opts.pidFile) : join10(process.cwd(), ".sechroom", "fleet.pid")
4904
+ );
4905
+ const logFile = resolve3(
4906
+ opts.logFile ? String(opts.logFile) : join10(process.cwd(), ".sechroom", "fleet.log")
4907
+ );
4908
+ if (detach && !isDetachedChild) {
4909
+ await readFleetConfig(String(opts.config));
4910
+ const pid = launchDetachedSupervisor({ logFile, pidFile, log });
4911
+ log(`detached supervisor started \u2014 pid ${pid}`);
4912
+ log(` logs \u2192 ${logFile}`);
4913
+ log(` pid-file \u2192 ${pidFile}`);
4914
+ log(` adopt it from any session via the pid-file above.`);
4915
+ return;
4916
+ }
4917
+ const config2 = await readFleetConfig(String(opts.config));
4918
+ const cfg = resolveConfig(cmd.optsWithGlobals());
4919
+ const activationMode = isDetachedChild ? "Detached" : "Attached";
3422
4920
  let nodeId;
3423
4921
  let stopNodeHeartbeat;
3424
4922
  if (config2.node) {
3425
- const node = await registerFleetNode(cfg, config2.node);
4923
+ const node = await registerFleetNode(cfg, config2.node, activationMode);
3426
4924
  nodeId = node.id;
3427
4925
  const nodeTtl = config2.node.ttl ?? 120;
3428
4926
  log(`node registered \u2014 ${node.id} (${config2.node.instanceKey})`);
@@ -3432,8 +4930,47 @@ function registerExecutorRunCommand(executor) {
3432
4930
  { onError: (e) => log(`node refresh failed: ${String(e)}`) }
3433
4931
  );
3434
4932
  }
3435
- const fleet = superviseFleet(config2, { parentId: nodeId });
3436
- const stop = () => void fleet.shutdown("SIGINT");
4933
+ let wakeNodeRouter = () => {
4934
+ };
4935
+ const fleet = superviseFleet(config2, {
4936
+ parentId: nodeId,
4937
+ activationMode,
4938
+ onRoutingSignal: () => wakeNodeRouter()
4939
+ });
4940
+ const nodeRouter = nodeId ? startNodeOfferRouter({
4941
+ request: createAuthedRequest(cfg),
4942
+ nodeId,
4943
+ routeOffer: fleet.routeOffer,
4944
+ log
4945
+ }) : void 0;
4946
+ wakeNodeRouter = () => nodeRouter?.wake();
4947
+ let controlServer;
4948
+ if (isDetachedChild) {
4949
+ try {
4950
+ controlServer = await startControlServer(
4951
+ controlSocketPath(pidFile),
4952
+ {
4953
+ status: () => ({
4954
+ supervisorPid: process.pid,
4955
+ node: nodeId && config2.node ? { instanceKey: config2.node.instanceKey, id: nodeId } : null,
4956
+ children: fleet.snapshot()
4957
+ }),
4958
+ restart: (instanceKey) => fleet.restartChild(instanceKey),
4959
+ stop: () => void fleet.shutdown("SIGINT")
4960
+ },
4961
+ { log }
4962
+ );
4963
+ log(`control socket \u2192 ${controlServer.socketPath}`);
4964
+ } catch (e) {
4965
+ log(
4966
+ `control socket unavailable (${String(e)}) \u2014 status/restart/stop verbs cannot reach this supervisor (SIGTERM to the pid still drains it)`
4967
+ );
4968
+ }
4969
+ }
4970
+ const stop = () => {
4971
+ void nodeRouter?.stop();
4972
+ void fleet.shutdown("SIGINT");
4973
+ };
3437
4974
  process.once("SIGINT", stop);
3438
4975
  process.once("SIGTERM", stop);
3439
4976
  try {
@@ -3441,6 +4978,10 @@ function registerExecutorRunCommand(executor) {
3441
4978
  } finally {
3442
4979
  process.off("SIGINT", stop);
3443
4980
  process.off("SIGTERM", stop);
4981
+ await nodeRouter?.stop().catch(() => {
4982
+ });
4983
+ await controlServer?.close().catch(() => {
4984
+ });
3444
4985
  stopNodeHeartbeat?.();
3445
4986
  if (nodeId) {
3446
4987
  try {
@@ -3452,6 +4993,93 @@ function registerExecutorRunCommand(executor) {
3452
4993
  );
3453
4994
  }
3454
4995
  }
4996
+ if (isDetachedChild) removePidFileIfOwned(pidFile, process.pid, log);
4997
+ }
4998
+ });
4999
+ fleetCommand.command("status").description(
5000
+ "Report a detached fleet's per-child state (live/exited/restarting), restart counts, and pids"
5001
+ ).option(
5002
+ "--pid-file <file>",
5003
+ "Supervisor pid-file to adopt (default .sechroom/fleet.pid)"
5004
+ ).action(async (_opts, cmd) => {
5005
+ const globals = cmd.optsWithGlobals();
5006
+ const pidFile = resolveFleetPidFile(globals.pidFile);
5007
+ requireLiveSupervisor(pidFile);
5008
+ const reply = await sendFleetCommand(pidFile, { command: "status" });
5009
+ if (!reply.ok || reply.command !== "status")
5010
+ fail(
5011
+ `fleet status failed: ${reply.ok ? "unexpected reply" : reply.error}`
5012
+ );
5013
+ if (globals.json) {
5014
+ process.stdout.write(`${JSON.stringify(reply.status, null, 2)}
5015
+ `);
5016
+ return;
5017
+ }
5018
+ renderFleetStatus(reply.status);
5019
+ });
5020
+ fleetCommand.command("restart").description(
5021
+ "Bounce exactly one child of a detached fleet by instanceKey, leaving its siblings untouched"
5022
+ ).argument("<instanceKey>", "The child instanceKey to restart").option(
5023
+ "--pid-file <file>",
5024
+ "Supervisor pid-file to adopt (default .sechroom/fleet.pid)"
5025
+ ).action(async (instanceKey, _opts, cmd) => {
5026
+ const pidFile = resolveFleetPidFile(cmd.optsWithGlobals().pidFile);
5027
+ requireLiveSupervisor(pidFile);
5028
+ const reply = await sendFleetCommand(pidFile, {
5029
+ command: "restart",
5030
+ instanceKey
5031
+ });
5032
+ if (!reply.ok) fail(`fleet restart failed: ${reply.error}`);
5033
+ const message = reply.command === "restart" ? reply.message : "restarted";
5034
+ process.stdout.write(
5035
+ `${style.green("restarted")} ${instanceKey} \u2014 ${message}
5036
+ `
5037
+ );
5038
+ });
5039
+ fleetCommand.command("stop").description(
5040
+ "Drain a detached fleet's children, retire the node last, then stop the supervisor"
5041
+ ).option(
5042
+ "--pid-file <file>",
5043
+ "Supervisor pid-file to adopt (default .sechroom/fleet.pid)"
5044
+ ).option(
5045
+ "--timeout <seconds>",
5046
+ "Seconds to wait for the supervisor to exit before reporting a slow drain",
5047
+ "60"
5048
+ ).action(async (opts, cmd) => {
5049
+ const pidFile = resolveFleetPidFile(cmd.optsWithGlobals().pidFile);
5050
+ const pid = requireLiveSupervisor(pidFile);
5051
+ try {
5052
+ const reply = await sendControlCommand(controlSocketPath(pidFile), {
5053
+ command: "stop"
5054
+ });
5055
+ if (!reply.ok) fail(`fleet stop failed: ${reply.error}`);
5056
+ } catch (e) {
5057
+ if (e instanceof FleetControlUnreachableError) {
5058
+ process.stderr.write(
5059
+ `control socket unreachable (${e.message}); sending SIGTERM to pid ${pid}
5060
+ `
5061
+ );
5062
+ try {
5063
+ process.kill(pid, "SIGTERM");
5064
+ } catch (killErr) {
5065
+ fail(`could not signal supervisor pid ${pid}: ${String(killErr)}`);
5066
+ }
5067
+ } else {
5068
+ fail(`fleet stop failed: ${String(e)}`);
5069
+ }
5070
+ }
5071
+ const timeoutMs = Number.parseInt(String(opts.timeout), 10) * 1e3;
5072
+ const drained = await waitForSupervisorExit(pidFile, pid, timeoutMs);
5073
+ if (drained) {
5074
+ process.stdout.write(
5075
+ `${style.green("stopped")} \u2014 children drained, node retired, supervisor exited
5076
+ `
5077
+ );
5078
+ } else {
5079
+ process.stderr.write(
5080
+ `${style.yellow("warning")}: supervisor pid ${pid} still running after ${opts.timeout}s \u2014 it may be finishing an in-flight turn; re-check with \`fleet status\`
5081
+ `
5082
+ );
3455
5083
  }
3456
5084
  });
3457
5085
  executor.command("run").description(
@@ -3476,6 +5104,14 @@ function registerExecutorRunCommand(executor) {
3476
5104
  ).option(
3477
5105
  "--parent-id <id>",
3478
5106
  "Fleet node this child enrolls under (D-WLP-55 containment; set by the fleet supervisor)"
5107
+ ).option(
5108
+ "--fleet-child",
5109
+ "Internal: accept node-routed claims over the fleet supervisor IPC channel",
5110
+ false
5111
+ ).option(
5112
+ "--activation-mode <mode>",
5113
+ "attached | detached \u2014 set by the fleet supervisor so a detached service's children read honestly on the roster (default attached)",
5114
+ "attached"
3479
5115
  ).option("--ttl <seconds>", "Advertisement TTL (30-600)").option("--once", "Process a single task, then exit", false).option(
3480
5116
  "--no-deliver",
3481
5117
  "Skip driver-side delivery (branch/commit/push of the turn's changes)"
@@ -3483,7 +5119,11 @@ function registerExecutorRunCommand(executor) {
3483
5119
  "--allow-dirty-root",
3484
5120
  "Claim even when the root has uncommitted changes (they are fenced out of the delivery commit)",
3485
5121
  false
3486
- ).option("--no-pr", "Deliver without raising a PR (branch + push only)").option("--poll-interval <seconds>", "Offer reconciliation interval", "5").option("--heartbeat-interval <seconds>", "Lease heartbeat cadence", "30").option("--turn-timeout <seconds>", "Fresh turn timeout", "300").option("--resume-turn-timeout <seconds>", "Resumed turn timeout", "1200").option(
5122
+ ).option("--no-pr", "Deliver without raising a PR (branch + push only)").option("--poll-interval <seconds>", "Offer reconciliation interval", "5").option(
5123
+ "--capacity-interval <seconds>",
5124
+ "Codex account-capacity snapshot cadence",
5125
+ "300"
5126
+ ).option("--heartbeat-interval <seconds>", "Lease heartbeat cadence", "30").option("--turn-timeout <seconds>", "Fresh turn timeout", "300").option("--resume-turn-timeout <seconds>", "Resumed turn timeout", "1200").option(
3487
5127
  "--drain-timeout <seconds>",
3488
5128
  "On shutdown, seconds to let an in-flight turn finish before interrupting",
3489
5129
  "30"
@@ -3491,12 +5131,15 @@ function registerExecutorRunCommand(executor) {
3491
5131
  "--usage-reserve <percent>",
3492
5132
  "Rate-limit reserve: defer claiming new tasks while remaining is at or below this percent",
3493
5133
  "2"
5134
+ ).option(
5135
+ "--exclude-tag <tag...>",
5136
+ "Skip claiming any offer whose task carries this tag (exact match, e.g. lane:ux); driver-side filter, repeatable"
3494
5137
  ).action(async (opts, cmd) => {
3495
5138
  if (String(opts.runtime).toLowerCase() !== "codex")
3496
5139
  fail(
3497
5140
  "executor run drives runtime codex only (claude-code stays attached)"
3498
5141
  );
3499
- const located = readExecutorState();
5142
+ const located = readExecutorState(resolve3(String(opts.root)));
3500
5143
  if (!located)
3501
5144
  fail(
3502
5145
  "executor run requires an installed executor advertisement; run `sechroom executor install` first."
@@ -3511,7 +5154,11 @@ function registerExecutorRunCommand(executor) {
3511
5154
  laneId: opts.lane ? String(opts.lane) : located.state.laneId,
3512
5155
  connectorId: opts.connector ? String(opts.connector) : located.state.connectorId,
3513
5156
  parentId: opts.parentId ? String(opts.parentId) : located.state.parentId,
3514
- ttlSeconds: opts.ttl ? Number.parseInt(String(opts.ttl), 10) : located.state.ttlSeconds
5157
+ ttlSeconds: opts.ttl ? Number.parseInt(String(opts.ttl), 10) : located.state.ttlSeconds,
5158
+ // Driver-side claim exclusion: the flag (repeatable) overlays the installed
5159
+ // state (flag wins when present), else the persisted excludeTags carry through.
5160
+ // Local-only — never sent in the advertisement (server schema untouched).
5161
+ excludeTags: opts.excludeTag ?? located.state.excludeTags
3515
5162
  };
3516
5163
  const heartbeatMs = Number.parseInt(String(opts.heartbeatInterval), 10) * 1e3;
3517
5164
  if (heartbeatMs >= 12e4)
@@ -3519,13 +5166,25 @@ function registerExecutorRunCommand(executor) {
3519
5166
  const usageReserve = Number.parseFloat(String(opts.usageReserve));
3520
5167
  if (!Number.isFinite(usageReserve) || usageReserve < 0 || usageReserve >= 100)
3521
5168
  fail("--usage-reserve must be a percent in [0, 100)");
5169
+ const capacityIntervalMs = Number.parseInt(String(opts.capacityInterval), 10) * 1e3;
5170
+ if (!Number.isFinite(capacityIntervalMs) || capacityIntervalMs < 6e4)
5171
+ fail("--capacity-interval must be at least 60 seconds");
3522
5172
  const cfg = resolveConfig(cmd.optsWithGlobals());
3523
- const instance = await ensureExecutorInstance(cfg, located);
5173
+ const activationMode = parseActivationMode(String(opts.activationMode));
5174
+ const instance = await ensureExecutorInstance(
5175
+ cfg,
5176
+ located,
5177
+ activationMode
5178
+ );
3524
5179
  const log = (line) => process.stderr.write(style.dim(`[run] ${line}
3525
5180
  `));
3526
5181
  const request = createAuthedRequest(cfg);
3527
- const rootDir = resolve2(String(opts.root));
3528
- const usageLogPath = join9(
5182
+ const excludeTags = located.state.excludeTags ?? [];
5183
+ const skipLog = /* @__PURE__ */ new Set();
5184
+ if (excludeTags.length)
5185
+ log(`excluding offers tagged: ${excludeTags.join(", ")}`);
5186
+ const rootDir = resolve3(String(opts.root));
5187
+ const usageLogPath = join10(
3529
5188
  rootDir,
3530
5189
  ".sechroom",
3531
5190
  `executor-usage-${located.state.instanceKey.replace(/[^\w.-]/g, "-")}.jsonl`
@@ -3536,7 +5195,27 @@ function registerExecutorRunCommand(executor) {
3536
5195
  log,
3537
5196
  appendRecord: createUsageLogAppender(usageLogPath, log)
3538
5197
  });
3539
- const appServer = new CodexAppServer({
5198
+ const fleetIpc = Boolean(opts.fleetChild && process.send);
5199
+ let advertisementExpiresAtMs = Date.parse(instance.expiresAt);
5200
+ if (!Number.isFinite(advertisementExpiresAtMs))
5201
+ advertisementExpiresAtMs = 0;
5202
+ let admission = { ok: true };
5203
+ let activeRoutedLeaseId;
5204
+ let appServer;
5205
+ const fleetInbox = createFleetChildRouteInbox({
5206
+ enabled: fleetIpc,
5207
+ instanceKey: located.state.instanceKey,
5208
+ instanceId: instance.id,
5209
+ localStatus: () => ({ advertisementExpiresAtMs, admission }),
5210
+ send: (message) => process.send?.(message),
5211
+ subscribe: (listener) => process.on("message", listener),
5212
+ unsubscribe: (listener) => process.off("message", listener),
5213
+ onClaimRelease: (leaseId, reason) => {
5214
+ log(`fleet route ${leaseId} released by node (${reason})`);
5215
+ if (activeRoutedLeaseId === leaseId) void appServer.interrupt();
5216
+ }
5217
+ });
5218
+ appServer = new CodexAppServer({
3540
5219
  codexBin: String(opts.codexBin),
3541
5220
  cwd: rootDir,
3542
5221
  model: opts.model ? String(opts.model) : void 0,
@@ -3551,6 +5230,12 @@ function registerExecutorRunCommand(executor) {
3551
5230
  });
3552
5231
  await appServer.start();
3553
5232
  log(`codex app-server up (${String(opts.codexBin)})`);
5233
+ let signalCapacityTerminal;
5234
+ const capacityTerminal = new Promise((resolve6) => {
5235
+ signalCapacityTerminal = resolve6;
5236
+ });
5237
+ let stopCapacityCapture = () => {
5238
+ };
3554
5239
  let stopping = false;
3555
5240
  let turnInFlight = false;
3556
5241
  const requestStop = () => {
@@ -3567,8 +5252,8 @@ function registerExecutorRunCommand(executor) {
3567
5252
  process.once("SIGTERM", requestStop);
3568
5253
  let wake = () => {
3569
5254
  };
3570
- const wakeSignal = () => new Promise((resolve5) => {
3571
- wake = resolve5;
5255
+ const wakeSignal = () => new Promise((resolve6) => {
5256
+ wake = resolve6;
3572
5257
  });
3573
5258
  let connStop = async () => {
3574
5259
  };
@@ -3579,13 +5264,19 @@ function registerExecutorRunCommand(executor) {
3579
5264
  log(`SignalR wake leg unavailable (${String(e)}) \u2014 poll-only`);
3580
5265
  }
3581
5266
  const stopAdvertisementHeartbeat = startExecutorHeartbeat(
3582
- () => request(
3583
- `/me/executor-instances/${encodeURIComponent(instance.id)}/refresh`,
3584
- {
3585
- method: "POST",
3586
- body: JSON.stringify({ ttlSeconds: located.state.ttlSeconds })
3587
- }
3588
- ),
5267
+ async () => {
5268
+ const refreshed = await request(
5269
+ `/me/executor-instances/${encodeURIComponent(instance.id)}/refresh`,
5270
+ {
5271
+ method: "POST",
5272
+ body: JSON.stringify({ ttlSeconds: located.state.ttlSeconds })
5273
+ }
5274
+ );
5275
+ advertisementExpiresAtMs = Date.parse(refreshed.expiresAt);
5276
+ if (!Number.isFinite(advertisementExpiresAtMs))
5277
+ advertisementExpiresAtMs = 0;
5278
+ fleetInbox.report();
5279
+ },
3589
5280
  located.state.refreshAfterSeconds * 1e3,
3590
5281
  { onError: (e) => log(`advertisement refresh failed: ${String(e)}`) }
3591
5282
  );
@@ -3600,8 +5291,9 @@ function registerExecutorRunCommand(executor) {
3600
5291
  rootSnapshot = ready.snapshot;
3601
5292
  return { ok: ready.ok, reason: ready.reason };
3602
5293
  },
3603
- deliver: (claim, task, verdict) => deliverTurn(gitRunner, {
5294
+ deliver: (claim, task, verdict, turnFileChangeCount) => deliverTurn(gitRunner, {
3604
5295
  taskId: claim.memoryId,
5296
+ turnFileChangeCount,
3605
5297
  title: task.title,
3606
5298
  verdict,
3607
5299
  snapshot: rootSnapshot ?? {
@@ -3615,8 +5307,12 @@ function registerExecutorRunCommand(executor) {
3615
5307
  };
3616
5308
  const ports = {
3617
5309
  ...deliveryPorts,
3618
- checkAdmission: async () => usageTracker.admission(),
3619
- claimNext: () => claimNext(request, instance.id, log),
5310
+ checkAdmission: async () => {
5311
+ admission = usageTracker.admission();
5312
+ fleetInbox.report();
5313
+ return admission;
5314
+ },
5315
+ claimNext: async () => await fleetInbox.waitForClaim() ?? await claimNext(request, instance.id, log, excludeTags, skipLog),
3620
5316
  loadTask: (memoryId) => loadTask(request, memoryId, log),
3621
5317
  startLeaseHeartbeat: (claim) => startLeaseHeartbeat(
3622
5318
  () => request(
@@ -3633,18 +5329,29 @@ function registerExecutorRunCommand(executor) {
3633
5329
  heartbeatMs
3634
5330
  ),
3635
5331
  runTurn: async (task, claim) => {
5332
+ if (fleetInbox.consumeClaimRelease(claim.leaseId))
5333
+ throw new Error(
5334
+ `fleet route ${claim.leaseId} was released before execution`
5335
+ );
3636
5336
  if (!appServer.alive) {
3637
5337
  log("codex app-server died between tasks \u2014 respawning");
3638
5338
  await appServer.start();
3639
5339
  }
5340
+ activeRoutedLeaseId = claim.executorInstanceId ? claim.leaseId : void 0;
3640
5341
  turnInFlight = true;
3641
5342
  try {
3642
- return await appServer.runTask(taskPrompt(task), {
5343
+ const result = await appServer.runTask(taskPrompt(task), {
3643
5344
  taskId: claim.memoryId,
3644
5345
  leaseId: claim.leaseId,
3645
5346
  decompositionId: claim.decompositionId
3646
5347
  });
5348
+ if (fleetInbox.consumeClaimRelease(claim.leaseId))
5349
+ throw new Error(
5350
+ `fleet route ${claim.leaseId} was released during execution`
5351
+ );
5352
+ return result;
3647
5353
  } finally {
5354
+ activeRoutedLeaseId = void 0;
3648
5355
  turnInFlight = false;
3649
5356
  }
3650
5357
  },
@@ -3653,7 +5360,7 @@ function registerExecutorRunCommand(executor) {
3653
5360
  {
3654
5361
  method: "POST",
3655
5362
  body: JSON.stringify({
3656
- executorInstanceId: instance.id,
5363
+ executorInstanceId: claim.executorInstanceId ?? instance.id,
3657
5364
  claimToken: claim.claimToken,
3658
5365
  tokenVersion: claim.tokenVersion,
3659
5366
  verdict,
@@ -3665,8 +5372,8 @@ function registerExecutorRunCommand(executor) {
3665
5372
  ),
3666
5373
  log,
3667
5374
  waitForWake: (ms) => Promise.race([
3668
- new Promise((resolve5) => {
3669
- setTimeout(resolve5, ms).unref?.();
5375
+ new Promise((resolve6) => {
5376
+ setTimeout(resolve6, ms).unref?.();
3670
5377
  }),
3671
5378
  wakeSignal()
3672
5379
  ])
@@ -3674,14 +5381,27 @@ function registerExecutorRunCommand(executor) {
3674
5381
  log(
3675
5382
  `driven executor live \u2014 instance ${located.state.instanceKey}, lane ${located.state.laneId ?? located.state.instanceKey}${opts.once ? ", single-task mode" : ""}`
3676
5383
  );
5384
+ fleetInbox.report();
3677
5385
  log(`usage log \u2192 ${usageLogPath}; admission reserve ${usageReserve}%`);
3678
5386
  try {
3679
- const summary = await runDriverLoop(ports, {
3680
- once: Boolean(opts.once),
3681
- pollMs: Number.parseInt(String(opts.pollInterval), 10) * 1e3,
3682
- stopping: () => stopping,
3683
- source: located.state.laneId ?? located.state.instanceKey
5387
+ stopCapacityCapture = startCodexAccountCapacityCapture({
5388
+ read: () => appServer.readAccountCapacitySignal(),
5389
+ persist: (snapshot) => postCodexAccountCapacity(request, snapshot),
5390
+ log,
5391
+ onTerminal: signalCapacityTerminal,
5392
+ intervalMs: capacityIntervalMs
3684
5393
  });
5394
+ const summary = await Promise.race([
5395
+ runDriverLoop(ports, {
5396
+ once: Boolean(opts.once),
5397
+ pollMs: Number.parseInt(String(opts.pollInterval), 10) * 1e3,
5398
+ stopping: () => stopping,
5399
+ source: located.state.laneId ?? located.state.instanceKey
5400
+ }),
5401
+ capacityTerminal.then((error) => {
5402
+ throw error;
5403
+ })
5404
+ ]);
3685
5405
  log(
3686
5406
  `done \u2014 processed ${summary.processed}, completed ${summary.completed}, abandoned ${summary.abandoned}`
3687
5407
  );
@@ -3692,10 +5412,16 @@ function registerExecutorRunCommand(executor) {
3692
5412
  `)
3693
5413
  );
3694
5414
  process.exitCode = 1;
5415
+ } else if (error instanceof BackoffCeilingExhaustedError) {
5416
+ process.stderr.write(style.dim(`[run] ${error.message}
5417
+ `));
5418
+ process.exitCode = 1;
3695
5419
  } else {
3696
5420
  throw error;
3697
5421
  }
3698
5422
  } finally {
5423
+ stopCapacityCapture();
5424
+ fleetInbox.close();
3699
5425
  stopAdvertisementHeartbeat();
3700
5426
  await connStop().catch(() => {
3701
5427
  });
@@ -3714,11 +5440,80 @@ function registerExecutorRunCommand(executor) {
3714
5440
  }
3715
5441
  });
3716
5442
  }
3717
- async function claimNext(request, instanceId, log) {
5443
+ function resolveFleetPidFile(flag) {
5444
+ return resolve3(
5445
+ flag ? String(flag) : join10(process.cwd(), ".sechroom", "fleet.pid")
5446
+ );
5447
+ }
5448
+ function requireLiveSupervisor(pidFile) {
5449
+ const pid = readPidFile(pidFile);
5450
+ if (pid === void 0)
5451
+ fail(
5452
+ `no fleet supervisor pid-file at ${pidFile}. Start one with \`sechroom executor fleet --config <file>\`, or pass --pid-file.`
5453
+ );
5454
+ if (!isProcessAlive(pid))
5455
+ fail(
5456
+ `pid-file ${pidFile} names pid ${pid}, which is not running \u2014 the supervisor crashed or was killed. Remove the stale pid-file or start a fresh fleet.`
5457
+ );
5458
+ return pid;
5459
+ }
5460
+ async function sendFleetCommand(pidFile, request) {
5461
+ try {
5462
+ return await sendControlCommand(controlSocketPath(pidFile), request);
5463
+ } catch (e) {
5464
+ if (e instanceof FleetControlUnreachableError)
5465
+ fail(
5466
+ `${e.message}. The supervisor is running but not serving its control socket; check its log.`
5467
+ );
5468
+ throw e;
5469
+ }
5470
+ }
5471
+ function renderFleetStatus(report) {
5472
+ const out = (line) => process.stdout.write(`${line}
5473
+ `);
5474
+ out(style.bold(`fleet supervisor pid ${report.supervisorPid}`));
5475
+ if (report.node) out(`node: ${report.node.instanceKey} (${report.node.id})`);
5476
+ const paint = (state) => {
5477
+ const cell = state.padEnd(10);
5478
+ if (state === "live") return style.green(cell);
5479
+ if (state === "restarting" || state === "stopping")
5480
+ return style.yellow(cell);
5481
+ return style.dim(cell);
5482
+ };
5483
+ if (report.children.length === 0) {
5484
+ out(style.dim("(no children)"));
5485
+ return;
5486
+ }
5487
+ const keyWidth = Math.max(
5488
+ ...report.children.map((c) => c.instanceKey.length),
5489
+ "instanceKey".length
5490
+ );
5491
+ out(
5492
+ `${"instanceKey".padEnd(keyWidth)} ${"state".padEnd(10)} ${"pid".padEnd(7)} restarts`
5493
+ );
5494
+ for (const child of report.children)
5495
+ out(
5496
+ `${child.instanceKey.padEnd(keyWidth)} ${paint(child.state)} ${String(child.pid ?? "\u2014").padEnd(7)} ${child.restarts}`
5497
+ );
5498
+ }
5499
+ async function waitForSupervisorExit(pidFile, pid, timeoutMs) {
5500
+ const deadline = Date.now() + timeoutMs;
5501
+ const sleep = (ms) => new Promise((res) => {
5502
+ setTimeout(res, ms).unref?.();
5503
+ });
5504
+ for (; ; ) {
5505
+ if (readPidFile(pidFile) !== pid || !isProcessAlive(pid)) return true;
5506
+ if (Date.now() >= deadline) return false;
5507
+ await sleep(200);
5508
+ }
5509
+ }
5510
+ async function claimNext(request, instanceId, log, excludeTags, skipLog) {
3718
5511
  const claimed = await claimNextTask({
3719
5512
  request,
3720
5513
  executorInstanceId: instanceId,
3721
- log
5514
+ log,
5515
+ excludeTags,
5516
+ skipLog
3722
5517
  });
3723
5518
  if (!claimed) return void 0;
3724
5519
  return {
@@ -3785,13 +5580,13 @@ function executorSubscriptionInput(name) {
3785
5580
  filter: { tags: ["kind:task"], workspaceScope: [] }
3786
5581
  };
3787
5582
  }
3788
- function executorRegistrationInput(state, deliverySubscriptionId) {
5583
+ function executorRegistrationInput(state, deliverySubscriptionId, activationMode = "Attached") {
3789
5584
  return {
3790
5585
  relayId: state.relayId,
3791
5586
  instanceKey: state.instanceKey,
3792
5587
  laneId: state.laneId ?? state.instanceKey,
3793
5588
  runtimeKind: parseRuntimeKind(state.runtime),
3794
- activationMode: "Attached",
5589
+ activationMode,
3795
5590
  deliverySubscriptionId,
3796
5591
  connectorId: state.connectorId,
3797
5592
  claimedCapabilityKeys: state.capabilityKeys,
@@ -3840,6 +5635,9 @@ function registerExecutor(program2) {
3840
5635
  ).option(
3841
5636
  "--claim-tag <tag...>",
3842
5637
  "Task tag this instance accepts under --claim-policy restricted"
5638
+ ).option(
5639
+ "--exclude-tag <tag...>",
5640
+ "Driver-side claim exclusion: `executor run` skips any offer whose task carries this tag (exact match, e.g. lane:ux)"
3843
5641
  ).option(
3844
5642
  "--relay <id>",
3845
5643
  "Relay identity shared by sibling instances",
@@ -3916,8 +5714,8 @@ function registerExecutor(program2) {
3916
5714
  if (opts.refreshAfter >= opts.ttl)
3917
5715
  fail("refresh-after must be shorter than the TTL");
3918
5716
  const sem = readSem();
3919
- const checkout = sem ? dirname7(dirname7(sem.path)) : process.cwd();
3920
- const statePath = join10(checkout, ".sechroom", EXECUTOR_STATE);
5717
+ const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
5718
+ const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
3921
5719
  const state = {
3922
5720
  schemaVersion: 1,
3923
5721
  instanceKey,
@@ -3927,20 +5725,21 @@ function registerExecutor(program2) {
3927
5725
  capabilityKeys: capabilities ?? [],
3928
5726
  claimPolicy: (opts.claimPolicy ?? "open").toLowerCase() === "restricted" ? "restricted" : "open",
3929
5727
  claimTags: opts.claimTag ?? [],
5728
+ excludeTags: opts.excludeTag ?? [],
3930
5729
  relayId: opts.relay,
3931
5730
  subscriptionName: opts.subscriptionName,
3932
5731
  ttlSeconds: opts.ttl,
3933
5732
  refreshAfterSeconds: opts.refreshAfter
3934
5733
  };
3935
5734
  if (!opts.dryRun) {
3936
- mkdirSync8(dirname7(statePath), { recursive: true });
3937
- writeFileSync7(statePath, JSON.stringify(state, null, 2) + "\n");
5735
+ mkdirSync9(dirname8(statePath), { recursive: true });
5736
+ writeFileSync8(statePath, JSON.stringify(state, null, 2) + "\n");
3938
5737
  ensureStateDirIgnored(checkout);
3939
5738
  }
3940
5739
  const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
3941
5740
  (target) => target.dir
3942
- ) : [join10(checkout, ".claude")];
3943
- const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join10(checkout, ".codex")];
5741
+ ) : [join11(checkout, ".claude")];
5742
+ const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
3944
5743
  const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
3945
5744
  for (const target of hookTargets) {
3946
5745
  const results = surface === "claude" ? [
@@ -3984,7 +5783,7 @@ function registerExecutor(program2) {
3984
5783
  );
3985
5784
  delete located.state.instanceId;
3986
5785
  delete located.state.lastRefreshAt;
3987
- writeFileSync7(
5786
+ writeFileSync8(
3988
5787
  located.path,
3989
5788
  JSON.stringify(located.state, null, 2) + "\n"
3990
5789
  );
@@ -4050,6 +5849,10 @@ function registerExecutor(program2) {
4050
5849
  ).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option(
4051
5850
  "--parent-id <id>",
4052
5851
  "Fleet node this instance enrolls under (D-WLP-55 containment; sets the roster parentId)"
5852
+ ).option(
5853
+ "--activation-mode <mode>",
5854
+ "attached | detached \u2014 detached marks a fleet run as a service that outlives its shell (default attached)",
5855
+ "attached"
4053
5856
  ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
4054
5857
  const cfg = resolveConfig(cmd.optsWithGlobals());
4055
5858
  const subscription = await api(
@@ -4076,7 +5879,7 @@ function registerExecutor(program2) {
4076
5879
  instanceKey: opts.instanceKey,
4077
5880
  laneId: opts.laneId ?? opts.instanceKey,
4078
5881
  runtimeKind: parseRuntimeKind(opts.runtime),
4079
- activationMode: "Attached",
5882
+ activationMode: parseActivationMode(opts.activationMode),
4080
5883
  deliverySubscriptionId: subscription.id,
4081
5884
  connectorId: opts.connector,
4082
5885
  claimedCapabilityKeys: opts.capability ?? [],
@@ -4124,6 +5927,96 @@ function registerExecutor(program2) {
4124
5927
  );
4125
5928
  emit(data, Boolean(cmd.optsWithGlobals().json));
4126
5929
  });
5930
+ executor.command("proxy-claim <generationId>").description(
5931
+ "Node-held proxy claim: hold a task lease on a child's behalf so an attended/harness session is board-visible (node = holder-of-record, child = worker)"
5932
+ ).requiredOption(
5933
+ "--node-instance <id>",
5934
+ "This node's executor-instance id (xins_\u2026) \u2014 the holder-of-record"
5935
+ ).requiredOption(
5936
+ "--proxied-for <id>",
5937
+ "The child executor-instance id (xins_\u2026) the node holds the lease for"
5938
+ ).action(async (generationId, opts, cmd) => {
5939
+ const cfg = resolveConfig(cmd.optsWithGlobals());
5940
+ const data = await api(cfg, "/me/executor-task-proxy-claims", {
5941
+ method: "POST",
5942
+ body: JSON.stringify({
5943
+ generationId,
5944
+ nodeInstanceId: opts.nodeInstance,
5945
+ proxiedForInstanceId: opts.proxiedFor
5946
+ })
5947
+ });
5948
+ emit(data, Boolean(cmd.optsWithGlobals().json));
5949
+ if (!cmd.optsWithGlobals().json)
5950
+ process.stderr.write(
5951
+ style.dim(
5952
+ "keep alive with: sechroom executor proxy-heartbeat <leaseId> --claim-token <token>\n"
5953
+ )
5954
+ );
5955
+ });
5956
+ executor.command("proxy-heartbeat <leaseId>").description(
5957
+ "Heartbeat a node-held task lease (keeps a proxied claim alive)"
5958
+ ).requiredOption(
5959
+ "--claim-token <token>",
5960
+ "The claim token returned by proxy-claim"
5961
+ ).option(
5962
+ "--token-version <n>",
5963
+ "Lease token version (default 1)",
5964
+ parseInteger,
5965
+ 1
5966
+ ).action(async (leaseId, opts, cmd) => {
5967
+ const cfg = resolveConfig(cmd.optsWithGlobals());
5968
+ const data = await api(
5969
+ cfg,
5970
+ `/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
5971
+ {
5972
+ method: "POST",
5973
+ body: JSON.stringify({
5974
+ claimToken: opts.claimToken,
5975
+ tokenVersion: opts.tokenVersion
5976
+ })
5977
+ }
5978
+ );
5979
+ emit(data, Boolean(cmd.optsWithGlobals().json));
5980
+ });
5981
+ executor.command("proxy-complete <leaseId>").description(
5982
+ "Complete a node-held task lease holder-bound (the closeout carries the child's identity via --source)"
5983
+ ).requiredOption(
5984
+ "--node-instance <id>",
5985
+ "The holder-of-record executor-instance id (this node)"
5986
+ ).requiredOption(
5987
+ "--claim-token <token>",
5988
+ "The claim token returned by proxy-claim"
5989
+ ).requiredOption(
5990
+ "--verdict <verdict>",
5991
+ "pass | soft-fail | plan-invalid | blocked"
5992
+ ).requiredOption("--text <text>", "Closeout memory text").requiredOption(
5993
+ "--source <lane>",
5994
+ "Closeout source lane \u2014 the child/worker identity (Gate resolution 3)"
5995
+ ).option("--title <title>", "Optional closeout title").option(
5996
+ "--token-version <n>",
5997
+ "Lease token version (default 1)",
5998
+ parseInteger,
5999
+ 1
6000
+ ).action(async (leaseId, opts, cmd) => {
6001
+ const cfg = resolveConfig(cmd.optsWithGlobals());
6002
+ const data = await api(
6003
+ cfg,
6004
+ `/me/executor-task-leases/${encodeURIComponent(leaseId)}/complete`,
6005
+ {
6006
+ method: "POST",
6007
+ body: JSON.stringify({
6008
+ executorInstanceId: opts.nodeInstance,
6009
+ claimToken: opts.claimToken,
6010
+ tokenVersion: opts.tokenVersion,
6011
+ verdict: opts.verdict,
6012
+ text: opts.text,
6013
+ source: opts.source,
6014
+ title: opts.title ?? null
6015
+ })
6016
+ }
6017
+ );
6018
+ emit(data, Boolean(cmd.optsWithGlobals().json));
6019
+ });
4127
6020
  executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
4128
6021
  const cfg = resolveConfig(cmd.optsWithGlobals());
4129
6022
  const data = await api(
@@ -4160,6 +6053,17 @@ function parseClaimPolicy(value) {
4160
6053
  return fail("claim-policy must be open or restricted");
4161
6054
  }
4162
6055
  }
6056
+ function parseActivationMode(value) {
6057
+ switch ((value ?? "attached").trim().toLowerCase()) {
6058
+ case "":
6059
+ case "attached":
6060
+ return "Attached";
6061
+ case "detached":
6062
+ return "Detached";
6063
+ default:
6064
+ return fail("activation-mode must be attached or detached");
6065
+ }
6066
+ }
4163
6067
  function parseTransport(value) {
4164
6068
  switch (value.trim().toLowerCase()) {
4165
6069
  case "push":
@@ -4180,7 +6084,7 @@ async function refreshExecutorInstance(cfg, id, ttlSeconds) {
4180
6084
  }
4181
6085
  );
4182
6086
  }
4183
- async function registerInstance(cfg, state) {
6087
+ async function registerInstance(cfg, state, activationMode = "Attached") {
4184
6088
  const subscription = await api(
4185
6089
  cfg,
4186
6090
  "/me/delivery-subscriptions/signalr",
@@ -4191,10 +6095,12 @@ async function registerInstance(cfg, state) {
4191
6095
  );
4192
6096
  return api(cfg, "/me/executor-instances", {
4193
6097
  method: "POST",
4194
- body: JSON.stringify(executorRegistrationInput(state, subscription.id))
6098
+ body: JSON.stringify(
6099
+ executorRegistrationInput(state, subscription.id, activationMode)
6100
+ )
4195
6101
  });
4196
6102
  }
4197
- async function registerFleetNode(cfg, node) {
6103
+ async function registerFleetNode(cfg, node, activationMode = "Attached") {
4198
6104
  const subscriptionName = node.subscriptionName ?? "executor-dispatch";
4199
6105
  const subscription = await api(
4200
6106
  cfg,
@@ -4211,7 +6117,7 @@ async function registerFleetNode(cfg, node) {
4211
6117
  instanceKey: node.instanceKey,
4212
6118
  laneId: node.lane ?? node.instanceKey,
4213
6119
  runtimeKind: "Node",
4214
- activationMode: "Attached",
6120
+ activationMode,
4215
6121
  deliverySubscriptionId: subscription.id,
4216
6122
  connectorId: node.connector,
4217
6123
  claimedCapabilityKeys: [],
@@ -4229,25 +6135,25 @@ async function deregisterInstance(cfg, id) {
4229
6135
  body: JSON.stringify({})
4230
6136
  });
4231
6137
  }
4232
- async function ensureExecutorInstance(cfg, located) {
6138
+ async function ensureExecutorInstance(cfg, located, activationMode = "Attached") {
4233
6139
  const { state, path } = located;
4234
6140
  state.laneId ??= state.instanceKey;
4235
- const data = await registerInstance(cfg, state);
6141
+ const data = await registerInstance(cfg, state, activationMode);
4236
6142
  state.instanceId = data.id;
4237
6143
  state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
4238
- writeFileSync7(path, JSON.stringify(state, null, 2) + "\n");
6144
+ writeFileSync8(path, JSON.stringify(state, null, 2) + "\n");
4239
6145
  return data;
4240
6146
  }
4241
6147
  function readExecutorState(start = process.cwd()) {
4242
6148
  const semPath = resolveSemPathForRead(start);
4243
6149
  const sem = semPath ? readSem(semPath) : void 0;
4244
- const path = join10(
4245
- sem ? dirname7(sem.path) : join10(start, ".sechroom"),
6150
+ const path = join11(
6151
+ sem ? dirname8(sem.path) : join11(start, ".sechroom"),
4246
6152
  EXECUTOR_STATE
4247
6153
  );
4248
6154
  if (!existsSync8(path)) return void 0;
4249
6155
  return {
4250
- state: JSON.parse(readFileSync6(path, "utf8")),
6156
+ state: JSON.parse(readFileSync7(path, "utf8")),
4251
6157
  path
4252
6158
  };
4253
6159
  }
@@ -4279,11 +6185,11 @@ function parseInteger(value) {
4279
6185
  return parsed;
4280
6186
  }
4281
6187
  function holdHeartbeat(tick, intervalMs) {
4282
- return new Promise((resolve5, reject) => {
6188
+ return new Promise((resolve6, reject) => {
4283
6189
  const timer = setInterval(() => void tick().catch(reject), intervalMs);
4284
6190
  const stop = () => {
4285
6191
  clearInterval(timer);
4286
- resolve5();
6192
+ resolve6();
4287
6193
  };
4288
6194
  process.once("SIGINT", stop);
4289
6195
  process.once("SIGTERM", stop);
@@ -4434,7 +6340,7 @@ function registerChannel(program2) {
4434
6340
  "MCP server + subscription name (idempotent per name)",
4435
6341
  "sechroom-channel"
4436
6342
  ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
4437
- const path = join11(process.cwd(), ".mcp.json");
6343
+ const path = join12(process.cwd(), ".mcp.json");
4438
6344
  const dryRun = Boolean(opts.dryRun);
4439
6345
  const args = ["channel", "mcp"];
4440
6346
  const entry = { command: "sechroom", args };
@@ -4444,8 +6350,8 @@ function registerChannel(program2) {
4444
6350
  const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
4445
6351
  if (status !== "current" && !dryRun) {
4446
6352
  config2.mcpServers[opts.name] = entry;
4447
- mkdirSync9(dirname8(path), { recursive: true });
4448
- writeFileSync8(path, JSON.stringify(config2, null, 2) + "\n");
6353
+ mkdirSync10(dirname9(path), { recursive: true });
6354
+ writeFileSync9(path, JSON.stringify(config2, null, 2) + "\n");
4449
6355
  }
4450
6356
  const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
4451
6357
  process.stdout.write(`${style.green("channel")} ${path} (${verb})
@@ -4536,7 +6442,7 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
4536
6442
  }
4537
6443
  async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
4538
6444
  const request = dependencies.request ?? api;
4539
- const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
6445
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
4540
6446
  const idempotencyKey = dependencies.idempotencyKey ?? ((offer) => `channel:${offer.generationId}`);
4541
6447
  const state = dependencies.state ?? {};
4542
6448
  for (; ; ) {
@@ -4582,7 +6488,7 @@ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {})
4582
6488
  }
4583
6489
  function readMcpConfig(path) {
4584
6490
  if (!existsSync9(path)) return {};
4585
- const raw = readFileSync7(path, "utf8");
6491
+ const raw = readFileSync8(path, "utf8");
4586
6492
  if (!raw.trim()) return {};
4587
6493
  try {
4588
6494
  return JSON.parse(raw);
@@ -4610,9 +6516,9 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
4610
6516
  return conn;
4611
6517
  }
4612
6518
  function holdOpen(conn) {
4613
- return new Promise((resolve5) => {
6519
+ return new Promise((resolve6) => {
4614
6520
  const stop = () => {
4615
- void conn.stop().finally(resolve5);
6521
+ void conn.stop().finally(resolve6);
4616
6522
  };
4617
6523
  process.on("SIGINT", stop);
4618
6524
  process.on("SIGTERM", stop);
@@ -4738,13 +6644,13 @@ Examples:
4738
6644
  }
4739
6645
 
4740
6646
  // src/commands/checkpoint.ts
4741
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
4742
- import { dirname as dirname10, join as join13 } from "path";
6647
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
6648
+ import { dirname as dirname11, join as join14 } from "path";
4743
6649
 
4744
6650
  // src/commands/hook.ts
4745
- import { createHash as createHash2 } from "crypto";
4746
- import { existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync9 } from "fs";
4747
- import { dirname as dirname9, join as join12 } from "path";
6651
+ import { createHash as createHash3 } from "crypto";
6652
+ import { existsSync as existsSync10, mkdirSync as mkdirSync11, readFileSync as readFileSync9, statSync as statSync2, writeFileSync as writeFileSync10 } from "fs";
6653
+ import { dirname as dirname10, join as join13 } from "path";
4748
6654
  async function readStdin2() {
4749
6655
  if (process.stdin.isTTY) return "";
4750
6656
  const chunks = [];
@@ -4768,13 +6674,13 @@ function resolveLane(flagLane, cwd) {
4768
6674
  if (!base) return void 0;
4769
6675
  return applyWorktreeLaneSuffix(base, start);
4770
6676
  }
4771
- var INTENT_FILE = join12(".sechroom", "continuity.json");
6677
+ var INTENT_FILE = join13(".sechroom", "continuity.json");
4772
6678
  function resolveIntentPath(start) {
4773
6679
  let dir = start;
4774
6680
  for (; ; ) {
4775
- const candidate = join12(dir, INTENT_FILE);
6681
+ const candidate = join13(dir, INTENT_FILE);
4776
6682
  if (existsSync10(candidate)) return candidate;
4777
- const parent = dirname9(dir);
6683
+ const parent = dirname10(dir);
4778
6684
  if (parent === dir) return void 0;
4779
6685
  dir = parent;
4780
6686
  }
@@ -4783,7 +6689,7 @@ function readIntent(start) {
4783
6689
  const path = resolveIntentPath(start);
4784
6690
  if (!path) return void 0;
4785
6691
  try {
4786
- return JSON.parse(readFileSync8(path, "utf8"));
6692
+ return JSON.parse(readFileSync9(path, "utf8"));
4787
6693
  } catch {
4788
6694
  return void 0;
4789
6695
  }
@@ -4825,14 +6731,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
4825
6731
  }
4826
6732
  function ledgerPath(start) {
4827
6733
  const intent = resolveIntentPath(start);
4828
- const dir = intent ? dirname9(intent) : join12(start, ".sechroom");
4829
- return join12(dir, ".checkpoint-state.json");
6734
+ const dir = intent ? dirname10(intent) : join13(start, ".sechroom");
6735
+ return join13(dir, ".checkpoint-state.json");
4830
6736
  }
4831
6737
  function readLedger(start) {
4832
6738
  try {
4833
6739
  const p = ledgerPath(start);
4834
6740
  if (!existsSync10(p)) return {};
4835
- return JSON.parse(readFileSync8(p, "utf8"));
6741
+ return JSON.parse(readFileSync9(p, "utf8"));
4836
6742
  } catch {
4837
6743
  return {};
4838
6744
  }
@@ -4851,7 +6757,7 @@ function intentHash(i) {
4851
6757
  artifacts: i.artifacts ?? [],
4852
6758
  confidence: i.confidence ?? null
4853
6759
  });
4854
- return createHash2("sha256").update(canonical, "utf8").digest("hex");
6760
+ return createHash3("sha256").update(canonical, "utf8").digest("hex");
4855
6761
  }
4856
6762
  function recentlyCheckpointed(start, minutes) {
4857
6763
  const { lastEpochMs } = readLedger(start);
@@ -4879,13 +6785,13 @@ function recordPush(start, intent) {
4879
6785
  } catch {
4880
6786
  mtimeMs = void 0;
4881
6787
  }
4882
- mkdirSync10(dirname9(p), { recursive: true });
6788
+ mkdirSync11(dirname10(p), { recursive: true });
4883
6789
  const ledger = {
4884
6790
  lastEpochMs: Date.now(),
4885
6791
  lastMtimeMs: mtimeMs,
4886
6792
  lastHash: intentHash(intent)
4887
6793
  };
4888
- writeFileSync9(p, JSON.stringify(ledger) + "\n");
6794
+ writeFileSync10(p, JSON.stringify(ledger) + "\n");
4889
6795
  } catch {
4890
6796
  }
4891
6797
  }
@@ -5138,10 +7044,10 @@ Examples:
5138
7044
  const client = await makeClient(cfg);
5139
7045
  return client.POST("/continuity/snapshots", { body });
5140
7046
  });
5141
- const path = resolveIntentPath(cwd) ?? join13(cwd, INTENT_FILE);
7047
+ const path = resolveIntentPath(cwd) ?? join14(cwd, INTENT_FILE);
5142
7048
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
5143
- mkdirSync11(dirname10(path), { recursive: true });
5144
- writeFileSync10(path, JSON.stringify(fileBody, null, 2) + "\n");
7049
+ mkdirSync12(dirname11(path), { recursive: true });
7050
+ writeFileSync11(path, JSON.stringify(fileBody, null, 2) + "\n");
5145
7051
  recordPush(cwd, merged);
5146
7052
  if (json) {
5147
7053
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -5155,7 +7061,7 @@ Examples:
5155
7061
  }
5156
7062
 
5157
7063
  // src/commands/close.ts
5158
- import { readFileSync as readFileSync9 } from "fs";
7064
+ import { readFileSync as readFileSync10 } from "fs";
5159
7065
  var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
5160
7066
  function registerClose(program2) {
5161
7067
  program2.command("close").description(
@@ -5196,7 +7102,7 @@ Examples:
5196
7102
  );
5197
7103
  let bodyText;
5198
7104
  try {
5199
- bodyText = opts.file ? readFileSync9(opts.file, "utf8") : readFileSync9(0, "utf8");
7105
+ bodyText = opts.file ? readFileSync10(opts.file, "utf8") : readFileSync10(0, "utf8");
5200
7106
  } catch {
5201
7107
  fail(
5202
7108
  opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
@@ -5619,6 +7525,26 @@ Examples:
5619
7525
  cmd.optsWithGlobals().json
5620
7526
  );
5621
7527
  });
7528
+ workPlan.command("accept-context-drift <decompositionId>").description(
7529
+ "Accept a parked run's exact stale-context delta, re-snapshot its task context pack, and resume dispatch"
7530
+ ).action(async (decompositionId, _opts, cmd) => {
7531
+ const cfg = resolveConfig(cmd.optsWithGlobals());
7532
+ const data = await runApi("Accepting context drift", async () => {
7533
+ const client = await makeClient(cfg);
7534
+ return client.POST(
7535
+ "/decompositions/{id}/accept-context-drift",
7536
+ {
7537
+ params: { path: { id: decompositionId } },
7538
+ body: {}
7539
+ }
7540
+ );
7541
+ });
7542
+ emitAction(
7543
+ `accepted context drift for ${style.bold(decompositionId)}`,
7544
+ data,
7545
+ cmd.optsWithGlobals().json
7546
+ );
7547
+ });
5622
7548
  workPlan.command("resume <decompositionId>").description(
5623
7549
  "Resume a failed work-plan decomposition (POST /decompositions/{id}/resume)"
5624
7550
  ).action(async (decompositionId, _opts, cmd) => {
@@ -5948,6 +7874,22 @@ Examples:
5948
7874
  }
5949
7875
 
5950
7876
  // src/commands/memory.ts
7877
+ import { readFileSync as readFileSync11 } from "fs";
7878
+ import { basename as basename2 } from "path";
7879
+ function resolveCreateBody(textOpt, fileOpt) {
7880
+ if (textOpt == null === (fileOpt == null)) {
7881
+ fail("Provide exactly one of --text or --file.");
7882
+ }
7883
+ if (textOpt != null) return { text: textOpt, defaultTitle: null };
7884
+ const fromStdin = fileOpt === "-";
7885
+ const text2 = fromStdin ? readFileSync11(0, "utf8") : readFileSync11(String(fileOpt), "utf8");
7886
+ if (text2.trim().length === 0) {
7887
+ fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
7888
+ }
7889
+ const heading = text2.match(/^#\s+(.+?)\s*$/m)?.[1];
7890
+ const defaultTitle = heading ?? (fromStdin ? null : basename2(String(fileOpt)).replace(/\.(md|markdown|txt)$/i, ""));
7891
+ return { text: text2, defaultTitle };
7892
+ }
5951
7893
  function registerMemory(program2) {
5952
7894
  const memory = program2.command("memory").description("Create, read, and search memories");
5953
7895
  memory.addHelpText(
@@ -5956,6 +7898,8 @@ function registerMemory(program2) {
5956
7898
  Examples:
5957
7899
  $ sechroom memory create --text "first note" --type reference --tag idea --tag cli
5958
7900
  $ sechroom memory create --text "filed note" --owner-type Workspace --owner-id wsp_XXXX
7901
+ $ sechroom memory create --file docs/conventions.md --owner-type Workspace --owner-id wsp_XXXX
7902
+ $ cat NOTES.md | sechroom memory create --file - --tag kind:reference
5959
7903
  $ sechroom memory search "rate limiting" --limit 5 --tag kind:decision
5960
7904
  $ sechroom memory search "auth flow" --workspace wsp_XXXX --json
5961
7905
  $ sechroom memory get mem_XXXX --json
@@ -5966,39 +7910,65 @@ Examples:
5966
7910
  $ sechroom memory list-archived --workspace wsp_XXXX --json
5967
7911
  $ sechroom memory tags --json`
5968
7912
  );
5969
- memory.command("create").description("Create a memory (POST /memories)").requiredOption("--text <text>", "Memory body text").option("--type <type>", "Memory type", "reference").option("--title <title>", "Optional title").option("--tag <tag...>", "Tags (repeatable)").option("--owner-type <ownerType>", "Workspace | Project | Unfiled", "Unfiled").option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
7913
+ memory.command("create").description("Create a memory (POST /memories)").option("--text <text>", "Memory body text").option(
7914
+ "--file <path>",
7915
+ "Read the body from a file (use - for stdin); .md files as-is"
7916
+ ).option("--type <type>", "Memory type", "reference").option(
7917
+ "--title <title>",
7918
+ "Optional title (with --file, defaults to the first # heading, else the filename)"
7919
+ ).option("--tag <tag...>", "Tags (repeatable)").option(
7920
+ "--owner-type <ownerType>",
7921
+ "Workspace | Project | Unfiled",
7922
+ "Unfiled"
7923
+ ).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
5970
7924
  const cfg = resolveConfig(cmd.optsWithGlobals());
7925
+ const { text: text2, defaultTitle } = resolveCreateBody(opts.text, opts.file);
7926
+ const title = opts.title ?? defaultTitle;
5971
7927
  const unfiled = String(opts.ownerType).toLowerCase() === "unfiled";
5972
7928
  const data = await runApi("Creating memory", async () => {
5973
7929
  const client = await makeClient(cfg);
5974
7930
  return client.POST("/memories", {
5975
7931
  body: {
5976
- text: opts.text,
7932
+ text: text2,
5977
7933
  type: opts.type,
5978
7934
  content: "{}",
5979
7935
  confidence: Number(opts.confidence),
5980
7936
  source: opts.source,
5981
7937
  archetype: "Document",
5982
- title: opts.title ?? null,
7938
+ title: title ?? null,
5983
7939
  tags: opts.tag ?? null,
5984
- owner: unfiled ? null : { type: opts.ownerType, id: String(opts.ownerId ?? "") }
7940
+ owner: unfiled ? null : {
7941
+ type: opts.ownerType,
7942
+ id: String(opts.ownerId ?? "")
7943
+ }
5985
7944
  }
5986
7945
  });
5987
7946
  });
5988
- const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
7947
+ const titlePart = title ? ` ${style.dim(`"${title}"`)}` : "";
5989
7948
  const view = resolveViewUrl(cfg.baseUrl, data.url);
5990
7949
  const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
5991
- emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
7950
+ emitAction(
7951
+ `created memory ${style.bold(data.id)}${titlePart}${urlPart}`,
7952
+ data,
7953
+ cmd.optsWithGlobals().json
7954
+ );
5992
7955
  });
5993
7956
  memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
5994
7957
  const cfg = resolveConfig(cmd.optsWithGlobals());
5995
7958
  const data = await runApi("Fetching memory", async () => {
5996
7959
  const client = await makeClient(cfg);
5997
- return client.GET("/memories/{memoryId}", { params: { path: { memoryId } } });
7960
+ return client.GET("/memories/{memoryId}", {
7961
+ params: { path: { memoryId } }
7962
+ });
5998
7963
  });
5999
7964
  emit(data, cmd.optsWithGlobals().json);
6000
7965
  });
6001
- memory.command("search <query>").description("Hybrid search (POST /memories/search; SemanticQuery -> vector+FTS RRF)").option("--limit <n>", "Max results", "10").option("--tag <tag...>", "Require all listed tags").option("--workspace <workspaceId>", "Scope to a workspace (cascades to its projects)").option("--include-archived", "Include archived memories", false).action(async (query, opts, cmd) => {
7966
+ memory.command("search <query>").description(
7967
+ "Hybrid search (POST /memories/search; SemanticQuery -> vector+FTS RRF)"
7968
+ ).option("--limit <n>", "Max results", "10").option("--tag <tag...>", "Require all listed tags").option(
7969
+ "--workspace <workspaceId>",
7970
+ "Scope to a workspace (cascades to its projects)"
7971
+ ).option("--include-archived", "Include archived memories", false).action(async (query, opts, cmd) => {
6002
7972
  const cfg = resolveConfig(cmd.optsWithGlobals());
6003
7973
  const data = await runApi("Searching", async () => {
6004
7974
  const client = await makeClient(cfg);
@@ -6018,7 +7988,13 @@ Examples:
6018
7988
  });
6019
7989
  emit(data, cmd.optsWithGlobals().json);
6020
7990
  });
6021
- memory.command("edit-text <memoryId>").description("Find/replace one substring (POST /memories/{memoryId}/edit-text)").requiredOption("--old <text>", "Text to find").requiredOption("--new <text>", "Replacement text").option("--replace-all", "Replace every occurrence (default: first only)", false).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
7991
+ memory.command("edit-text <memoryId>").description(
7992
+ "Find/replace one substring (POST /memories/{memoryId}/edit-text)"
7993
+ ).requiredOption("--old <text>", "Text to find").requiredOption("--new <text>", "Replacement text").option(
7994
+ "--replace-all",
7995
+ "Replace every occurrence (default: first only)",
7996
+ false
7997
+ ).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6022
7998
  const cfg = resolveConfig(cmd.optsWithGlobals());
6023
7999
  const data = await runApi("Editing memory text", async () => {
6024
8000
  const client = await makeClient(cfg);
@@ -6034,19 +8010,31 @@ Examples:
6034
8010
  }
6035
8011
  });
6036
8012
  });
6037
- emitAction(`edited ${style.bold(memoryId)} \u2192 v${style.bold(String(data.version))}`, data, cmd.optsWithGlobals().json);
8013
+ emitAction(
8014
+ `edited ${style.bold(memoryId)} \u2192 v${style.bold(String(data.version))}`,
8015
+ data,
8016
+ cmd.optsWithGlobals().json
8017
+ );
6038
8018
  });
6039
- memory.command("edit-text-batch <memoryId>").description("Apply many find/replace edits (POST /memories/{memoryId}/edit-text-batch)").requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
8019
+ memory.command("edit-text-batch <memoryId>").description(
8020
+ "Apply many find/replace edits (POST /memories/{memoryId}/edit-text-batch)"
8021
+ ).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6040
8022
  const cfg = resolveConfig(cmd.optsWithGlobals());
6041
8023
  const replaceAll = Boolean(opts.replaceAll);
6042
8024
  const edits = opts.edit.map((spec) => {
6043
8025
  const idx = spec.indexOf("=>");
6044
8026
  if (idx < 0) {
6045
- process.stderr.write(`error: --edit must be 'old=>new', got: ${spec}
6046
- `);
8027
+ process.stderr.write(
8028
+ `error: --edit must be 'old=>new', got: ${spec}
8029
+ `
8030
+ );
6047
8031
  process.exit(1);
6048
8032
  }
6049
- return { oldText: spec.slice(0, idx), newText: spec.slice(idx + 2), replaceAll };
8033
+ return {
8034
+ oldText: spec.slice(0, idx),
8035
+ newText: spec.slice(idx + 2),
8036
+ replaceAll
8037
+ };
6050
8038
  });
6051
8039
  const data = await runApi("Applying batch edits", async () => {
6052
8040
  const client = await makeClient(cfg);
@@ -6066,22 +8054,44 @@ Examples:
6066
8054
  cmd.optsWithGlobals().json
6067
8055
  );
6068
8056
  });
6069
- memory.command("update <memoryId>").description("Update metadata only \u2014 title/tags/type/confidence (PATCH /memories/{memoryId}/metadata; omitted = unchanged)").option("--title <text>", "Set the title").option("--tag <tag...>", "Set the full tag list (replaces existing); repeatable").option("--add-tag <tag...>", "Add tag(s) to the existing set (repeatable)").option("--remove-tag <tag...>", "Remove tag(s) from the existing set (repeatable)").option("--type <type>", "Set the memory type (e.g. reference, note, document)").option("--confidence <n>", "Set confidence (0..1)", (v) => Number(v)).option("--memory-source <src>", "Set the memory's own Source field").option("--bump-version", "Bump the version chain (use for a content reinterpretation, e.g. a type promotion)", false).option("--source <source>", "Contributing lane stamp (attribution)", "cli").action(async (memoryId, opts, cmd) => {
8057
+ memory.command("update <memoryId>").description(
8058
+ "Update metadata only \u2014 title/tags/type/confidence (PATCH /memories/{memoryId}/metadata; omitted = unchanged)"
8059
+ ).option("--title <text>", "Set the title").option(
8060
+ "--tag <tag...>",
8061
+ "Set the full tag list (replaces existing); repeatable"
8062
+ ).option("--add-tag <tag...>", "Add tag(s) to the existing set (repeatable)").option(
8063
+ "--remove-tag <tag...>",
8064
+ "Remove tag(s) from the existing set (repeatable)"
8065
+ ).option(
8066
+ "--type <type>",
8067
+ "Set the memory type (e.g. reference, note, document)"
8068
+ ).option("--confidence <n>", "Set confidence (0..1)", (v) => Number(v)).option("--memory-source <src>", "Set the memory's own Source field").option(
8069
+ "--bump-version",
8070
+ "Bump the version chain (use for a content reinterpretation, e.g. a type promotion)",
8071
+ false
8072
+ ).option("--source <source>", "Contributing lane stamp (attribution)", "cli").action(async (memoryId, opts, cmd) => {
6070
8073
  const cfg = resolveConfig(cmd.optsWithGlobals());
6071
8074
  const json = cmd.optsWithGlobals().json;
6072
8075
  const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
6073
8076
  const hasAny = opts.title !== void 0 || hasTagOps || opts.type !== void 0 || opts.confidence !== void 0 || opts.memorySource !== void 0 || Boolean(opts.bumpVersion);
6074
8077
  if (!hasAny)
6075
- fail("nothing to update \u2014 pass at least one of --title / --tag / --add-tag / --remove-tag / --type / --confidence / --memory-source.");
8078
+ fail(
8079
+ "nothing to update \u2014 pass at least one of --title / --tag / --add-tag / --remove-tag / --type / --confidence / --memory-source."
8080
+ );
6076
8081
  let tags;
6077
8082
  if (hasTagOps) {
6078
8083
  let base;
6079
8084
  if (opts.tag) base = opts.tag;
6080
8085
  else {
6081
- const current = await runApi("Reading current tags", async () => {
6082
- const client = await makeClient(cfg);
6083
- return client.GET("/memories/{memoryId}", { params: { path: { memoryId } } });
6084
- });
8086
+ const current = await runApi(
8087
+ "Reading current tags",
8088
+ async () => {
8089
+ const client = await makeClient(cfg);
8090
+ return client.GET("/memories/{memoryId}", {
8091
+ params: { path: { memoryId } }
8092
+ });
8093
+ }
8094
+ );
6085
8095
  base = current?.item?.tags ?? current?.tags ?? [];
6086
8096
  }
6087
8097
  const set = new Set(base);
@@ -6098,7 +8108,8 @@ Examples:
6098
8108
  if (tags !== void 0) body.tags = tags;
6099
8109
  if (opts.type !== void 0) body.type = opts.type;
6100
8110
  if (opts.confidence !== void 0) body.confidence = opts.confidence;
6101
- if (opts.memorySource !== void 0) body.memorySource = opts.memorySource;
8111
+ if (opts.memorySource !== void 0)
8112
+ body.memorySource = opts.memorySource;
6102
8113
  const data = await runApi("Updating metadata", async () => {
6103
8114
  const client = await makeClient(cfg);
6104
8115
  return client.PATCH("/memories/{memoryId}/metadata", {
@@ -6108,7 +8119,11 @@ Examples:
6108
8119
  });
6109
8120
  const changed = data?.changed ?? [];
6110
8121
  const summary = changed.length > 0 ? `updated ${changed.join(", ")}` : "no changes";
6111
- emitAction(`${summary} on ${style.bold(memoryId)} \u2192 v${style.bold(String(data?.version ?? "?"))}`, data, json);
8122
+ emitAction(
8123
+ `${summary} on ${style.bold(memoryId)} \u2192 v${style.bold(String(data?.version ?? "?"))}`,
8124
+ data,
8125
+ json
8126
+ );
6112
8127
  });
6113
8128
  memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6114
8129
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -6119,9 +8134,15 @@ Examples:
6119
8134
  body: { source: opts.source }
6120
8135
  });
6121
8136
  });
6122
- emitAction(`archived ${style.bold(memoryId)}`, data, cmd.optsWithGlobals().json);
8137
+ emitAction(
8138
+ `archived ${style.bold(memoryId)}`,
8139
+ data,
8140
+ cmd.optsWithGlobals().json
8141
+ );
6123
8142
  });
6124
- memory.command("restore <memoryId>").description("Restore an archived memory (POST /memories/{memoryId}/restore)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
8143
+ memory.command("restore <memoryId>").description(
8144
+ "Restore an archived memory (POST /memories/{memoryId}/restore)"
8145
+ ).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6125
8146
  const cfg = resolveConfig(cmd.optsWithGlobals());
6126
8147
  const data = await runApi("Restoring memory", async () => {
6127
8148
  const client = await makeClient(cfg);
@@ -6130,9 +8151,18 @@ Examples:
6130
8151
  body: { source: opts.source }
6131
8152
  });
6132
8153
  });
6133
- emitAction(`restored ${style.bold(memoryId)}`, data, cmd.optsWithGlobals().json);
8154
+ emitAction(
8155
+ `restored ${style.bold(memoryId)}`,
8156
+ data,
8157
+ cmd.optsWithGlobals().json
8158
+ );
6134
8159
  });
6135
- memory.command("move <memoryId>").description("Move a memory to a new owner (POST /memories/{memoryId}/move)").requiredOption("--owner-type <ownerType>", "Unfiled | Workspace | Project | Candidate").option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
8160
+ memory.command("move <memoryId>").description(
8161
+ "Move a memory to a new owner (POST /memories/{memoryId}/move)"
8162
+ ).requiredOption(
8163
+ "--owner-type <ownerType>",
8164
+ "Unfiled | Workspace | Project | Candidate"
8165
+ ).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6136
8166
  const cfg = resolveConfig(cmd.optsWithGlobals());
6137
8167
  const data = await runApi("Moving memory", async () => {
6138
8168
  const client = await makeClient(cfg);
@@ -6147,7 +8177,11 @@ Examples:
6147
8177
  }
6148
8178
  });
6149
8179
  });
6150
- emitAction(`moved ${style.bold(memoryId)} \u2192 ${opts.ownerType}`, data, cmd.optsWithGlobals().json);
8180
+ emitAction(
8181
+ `moved ${style.bold(memoryId)} \u2192 ${opts.ownerType}`,
8182
+ data,
8183
+ cmd.optsWithGlobals().json
8184
+ );
6151
8185
  });
6152
8186
  memory.command("list-archived").description("List archived memories (GET /memories/archived)").option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
6153
8187
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -6170,11 +8204,21 @@ Examples:
6170
8204
  const cfg = resolveConfig(cmd.optsWithGlobals());
6171
8205
  const data = await runApi("Fetching versions", async () => {
6172
8206
  const client = await makeClient(cfg);
6173
- return client.GET("/memories/{memoryId}/versions", { params: { path: { memoryId } } });
8207
+ return client.GET("/memories/{memoryId}/versions", {
8208
+ params: { path: { memoryId } }
8209
+ });
6174
8210
  });
6175
8211
  emit(data, cmd.optsWithGlobals().json);
6176
8212
  });
6177
- memory.command("revert <memoryId>").description("Revert a memory to an earlier version (POST /memories/{memoryId}/revert)").requiredOption("--from-version <n>", "Version to revert from").requiredOption("--text <text>", "Reverted text (the target version's text)").requiredOption("--content <content>", "Reverted content JSON (the target version's content)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
8213
+ memory.command("revert <memoryId>").description(
8214
+ "Revert a memory to an earlier version (POST /memories/{memoryId}/revert)"
8215
+ ).requiredOption("--from-version <n>", "Version to revert from").requiredOption(
8216
+ "--text <text>",
8217
+ "Reverted text (the target version's text)"
8218
+ ).requiredOption(
8219
+ "--content <content>",
8220
+ "Reverted content JSON (the target version's content)"
8221
+ ).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
6178
8222
  const cfg = resolveConfig(cmd.optsWithGlobals());
6179
8223
  const data = await runApi("Reverting memory", async () => {
6180
8224
  const client = await makeClient(cfg);
@@ -6188,7 +8232,11 @@ Examples:
6188
8232
  }
6189
8233
  });
6190
8234
  });
6191
- emitAction(`reverted ${style.bold(memoryId)} from v${opts.fromVersion}`, data, cmd.optsWithGlobals().json);
8235
+ emitAction(
8236
+ `reverted ${style.bold(memoryId)} from v${opts.fromVersion}`,
8237
+ data,
8238
+ cmd.optsWithGlobals().json
8239
+ );
6192
8240
  });
6193
8241
  memory.command("owners").description("List owners with memory counts (GET /memories/owners)").option("--include-archived", "Include archived memories in counts", false).action(async (opts, cmd) => {
6194
8242
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -6260,16 +8308,16 @@ Examples:
6260
8308
  }
6261
8309
 
6262
8310
  // src/setup/apply.ts
6263
- import { createHash as createHash3 } from "crypto";
6264
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync11, existsSync as existsSync11 } from "fs";
6265
- import { dirname as dirname11 } from "path";
8311
+ import { createHash as createHash4 } from "crypto";
8312
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
8313
+ import { dirname as dirname12 } from "path";
6266
8314
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
6267
8315
  var MARKER_END = "<!-- @sechroom/cli:end";
6268
8316
  function normalizeBody(s) {
6269
8317
  return s.replace(/\r\n/g, "\n").trim();
6270
8318
  }
6271
8319
  function bodySha256(body) {
6272
- return createHash3("sha256").update(normalizeBody(body), "utf8").digest("hex");
8320
+ return createHash4("sha256").update(normalizeBody(body), "utf8").digest("hex");
6273
8321
  }
6274
8322
  function renderBlock(write) {
6275
8323
  const body = normalizeBody(write.body);
@@ -6315,11 +8363,11 @@ function parseManagedBlock(content, block) {
6315
8363
  return null;
6316
8364
  }
6317
8365
  function ensureDir2(path) {
6318
- mkdirSync12(dirname11(path), { recursive: true });
8366
+ mkdirSync13(dirname12(path), { recursive: true });
6319
8367
  }
6320
8368
  function readOr(path, fallback) {
6321
8369
  try {
6322
- return readFileSync10(path, "utf8");
8370
+ return readFileSync12(path, "utf8");
6323
8371
  } catch {
6324
8372
  return fallback;
6325
8373
  }
@@ -6330,7 +8378,7 @@ function mergeMcpJson(path, snippet, dryRun) {
6330
8378
  let current = {};
6331
8379
  if (existed) {
6332
8380
  try {
6333
- current = JSON.parse(readFileSync10(path, "utf8"));
8381
+ current = JSON.parse(readFileSync12(path, "utf8"));
6334
8382
  } catch {
6335
8383
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
6336
8384
  }
@@ -6338,7 +8386,7 @@ function mergeMcpJson(path, snippet, dryRun) {
6338
8386
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
6339
8387
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
6340
8388
  ensureDir2(path);
6341
- writeFileSync11(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
8389
+ writeFileSync12(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
6342
8390
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
6343
8391
  }
6344
8392
  function mergeCodexToml(path, snippet, dryRun) {
@@ -6349,7 +8397,7 @@ function mergeCodexToml(path, snippet, dryRun) {
6349
8397
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
6350
8398
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
6351
8399
  ensureDir2(path);
6352
- writeFileSync11(path, next, { mode: 384 });
8400
+ writeFileSync12(path, next, { mode: 384 });
6353
8401
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
6354
8402
  }
6355
8403
  function writeInstructionBlock(path, write, dryRun) {
@@ -6357,7 +8405,7 @@ function writeInstructionBlock(path, write, dryRun) {
6357
8405
  const next = computeBlockFile(readOr(path, ""), write);
6358
8406
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
6359
8407
  ensureDir2(path);
6360
- writeFileSync11(path, next);
8408
+ writeFileSync12(path, next);
6361
8409
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
6362
8410
  }
6363
8411
  function computeBlockFile(current, write) {
@@ -6398,7 +8446,7 @@ function applyBlock(path, write, mode, dryRun) {
6398
8446
  const next = computeBlockFile(current, write);
6399
8447
  if (!dryRun) {
6400
8448
  ensureDir2(proposedPath);
6401
- writeFileSync11(proposedPath, next);
8449
+ writeFileSync12(proposedPath, next);
6402
8450
  }
6403
8451
  return {
6404
8452
  kind: "instruction",
@@ -6528,8 +8576,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
6528
8576
  }
6529
8577
 
6530
8578
  // src/setup/skills-offer.ts
6531
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync12 } from "fs";
6532
- import { join as join14 } from "path";
8579
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
8580
+ import { join as join15 } from "path";
6533
8581
 
6534
8582
  // src/setup/lane-pin.ts
6535
8583
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -6645,8 +8693,8 @@ Found ${summary} available to you for ${surface}.
6645
8693
  if (skills.length > 0) {
6646
8694
  const written = [];
6647
8695
  for (const s of skills) {
6648
- mkdirSync13(join14(sDir, s.name), { recursive: true });
6649
- writeFileSync12(join14(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
8696
+ mkdirSync14(join15(sDir, s.name), { recursive: true });
8697
+ writeFileSync13(join15(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
6650
8698
  written.push(s.name);
6651
8699
  }
6652
8700
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -6654,11 +8702,11 @@ Found ${summary} available to you for ${surface}.
6654
8702
  `);
6655
8703
  }
6656
8704
  if (agents.length > 0) {
6657
- mkdirSync13(aDir, { recursive: true });
8705
+ mkdirSync14(aDir, { recursive: true });
6658
8706
  const written = [];
6659
8707
  for (const a of agents) {
6660
8708
  const file = `${a.name}.md`;
6661
- writeFileSync12(join14(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
8709
+ writeFileSync13(join15(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
6662
8710
  written.push(file);
6663
8711
  }
6664
8712
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -7042,12 +9090,12 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
7042
9090
 
7043
9091
  // src/commands/onboard.ts
7044
9092
  import { existsSync as existsSync13 } from "fs";
7045
- import { basename as basename2, join as join16 } from "path";
9093
+ import { basename as basename3, join as join17 } from "path";
7046
9094
 
7047
9095
  // src/commands/fanout.ts
7048
9096
  import { spawnSync } from "child_process";
7049
- import { existsSync as existsSync12, readFileSync as readFileSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
7050
- import { isAbsolute, join as join15, resolve as resolve3 } from "path";
9097
+ import { existsSync as existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
9098
+ import { isAbsolute, join as join16, resolve as resolve4 } from "path";
7051
9099
  var ICON = {
7052
9100
  refresh: "\u21BB",
7053
9101
  bind: "+",
@@ -7055,7 +9103,7 @@ var ICON = {
7055
9103
  "skip-unbound": "\u26A0"
7056
9104
  };
7057
9105
  function resolveChildDir(path, root) {
7058
- return isAbsolute(path) ? path : resolve3(root, path);
9106
+ return isAbsolute(path) ? path : resolve4(root, path);
7059
9107
  }
7060
9108
  function discoverChildren(root) {
7061
9109
  let names;
@@ -7067,13 +9115,13 @@ function discoverChildren(root) {
7067
9115
  const out = [];
7068
9116
  for (const name of names.sort()) {
7069
9117
  if (name.startsWith(".") || name === "node_modules") continue;
7070
- const dir = join15(root, name);
9118
+ const dir = join16(root, name);
7071
9119
  try {
7072
9120
  if (!statSync3(dir).isDirectory()) continue;
7073
9121
  } catch {
7074
9122
  continue;
7075
9123
  }
7076
- if (existsSync12(join15(dir, ".git")) || committedBindingPath(dir)) out.push(name);
9124
+ if (existsSync12(join16(dir, ".git")) || committedBindingPath(dir)) out.push(name);
7077
9125
  }
7078
9126
  return out;
7079
9127
  }
@@ -7081,7 +9129,7 @@ function readManifest(path) {
7081
9129
  if (!existsSync12(path)) return null;
7082
9130
  let parsed;
7083
9131
  try {
7084
- parsed = JSON.parse(readFileSync11(path, "utf8"));
9132
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
7085
9133
  } catch (err2) {
7086
9134
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
7087
9135
  }
@@ -7251,7 +9299,7 @@ function personalSubtreeIds(personalId, all) {
7251
9299
  }
7252
9300
  async function pickWorkspace(client, opts = {}) {
7253
9301
  const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
7254
- const dirName = opts.dirName ?? basename2(process.cwd());
9302
+ const dirName = opts.dirName ?? basename3(process.cwd());
7255
9303
  const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
7256
9304
  if (all.length === 0) {
7257
9305
  process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
@@ -7302,7 +9350,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
7302
9350
  }
7303
9351
  if (existing) return existing;
7304
9352
  if (!canPrompt() || opts.yes) return void 0;
7305
- return pickWorkspace(client, { dirName: basename2(process.cwd()) });
9353
+ return pickWorkspace(client, { dirName: basename3(process.cwd()) });
7306
9354
  }
7307
9355
  async function ensureTenant(baseUrl, g, opts) {
7308
9356
  const persisted = readPersisted();
@@ -7450,7 +9498,7 @@ async function planRecurseChild(entry, root, client, opts) {
7450
9498
  if (!existsSync13(dir)) {
7451
9499
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
7452
9500
  }
7453
- if (existsSync13(join16(dir, ".sechroom.json"))) {
9501
+ if (existsSync13(join17(dir, ".sechroom.json"))) {
7454
9502
  return {
7455
9503
  label: entry.path,
7456
9504
  dir,
@@ -7479,7 +9527,7 @@ ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
7479
9527
  `);
7480
9528
  const ws = await pickWorkspace(client, {
7481
9529
  promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
7482
- dirName: basename2(entry.path)
9530
+ dirName: basename3(entry.path)
7483
9531
  });
7484
9532
  if (!ws) {
7485
9533
  return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
@@ -7523,7 +9571,7 @@ This fan-out will pin the same lane in every repo:
7523
9571
  async function runRecurse(cfg, g, opts) {
7524
9572
  const { yes, dryRun, json } = opts;
7525
9573
  const root = process.cwd();
7526
- const manifestPath = join16(root, ".sechroom", "repos.json");
9574
+ const manifestPath = join17(root, ".sechroom", "repos.json");
7527
9575
  const fromManifest = readManifest(manifestPath);
7528
9576
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
7529
9577
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -8055,32 +10103,32 @@ Examples:
8055
10103
  }
8056
10104
 
8057
10105
  // src/commands/reset.ts
8058
- import { homedir as homedir4 } from "os";
8059
- import { join as join17 } from "path";
8060
- import { existsSync as existsSync14, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
10106
+ import { homedir as homedir5 } from "os";
10107
+ import { join as join18 } from "path";
10108
+ import { existsSync as existsSync14, readFileSync as readFileSync14, rmSync as rmSync5 } from "fs";
8061
10109
  var SKILLS_LOCK2 = ".sechroom-skills.json";
8062
- var localSkillsDir = () => join17(process.cwd(), ".claude", "skills");
8063
- var globalSkillsDir = () => join17(homedir4(), ".claude", "skills");
8064
- var localAgentsDir = () => join17(process.cwd(), ".claude", "agents");
8065
- var globalAgentsDir = () => join17(homedir4(), ".claude", "agents");
10110
+ var localSkillsDir = () => join18(process.cwd(), ".claude", "skills");
10111
+ var globalSkillsDir = () => join18(homedir5(), ".claude", "skills");
10112
+ var localAgentsDir = () => join18(process.cwd(), ".claude", "agents");
10113
+ var globalAgentsDir = () => join18(homedir5(), ".claude", "agents");
8066
10114
  function removeMaterialisedSkills(dir) {
8067
10115
  const removed = [];
8068
- const lockPath = join17(dir, SKILLS_LOCK2);
10116
+ const lockPath = join18(dir, SKILLS_LOCK2);
8069
10117
  if (!existsSync14(lockPath)) return removed;
8070
10118
  try {
8071
- const lock = JSON.parse(readFileSync12(lockPath, "utf8"));
10119
+ const lock = JSON.parse(readFileSync14(lockPath, "utf8"));
8072
10120
  for (const entry of Object.values(lock)) {
8073
10121
  for (const name of entry.skills ?? []) {
8074
- const p = join17(dir, name);
10122
+ const p = join18(dir, name);
8075
10123
  if (existsSync14(p)) {
8076
- rmSync4(p, { recursive: true, force: true });
10124
+ rmSync5(p, { recursive: true, force: true });
8077
10125
  removed.push(p);
8078
10126
  }
8079
10127
  }
8080
10128
  }
8081
10129
  } catch {
8082
10130
  }
8083
- rmSync4(lockPath, { force: true });
10131
+ rmSync5(lockPath, { force: true });
8084
10132
  removed.push(lockPath);
8085
10133
  return removed;
8086
10134
  }
@@ -8117,19 +10165,19 @@ function registerReset(program2) {
8117
10165
  }
8118
10166
  }
8119
10167
  const removed = [];
8120
- const stateDir = join17(process.cwd(), ".sechroom");
10168
+ const stateDir = join18(process.cwd(), ".sechroom");
8121
10169
  if (existsSync14(stateDir)) {
8122
- rmSync4(stateDir, { recursive: true, force: true });
10170
+ rmSync5(stateDir, { recursive: true, force: true });
8123
10171
  removed.push(stateDir);
8124
10172
  }
8125
- const legacyCfg = join17(process.cwd(), ".sechroom.json");
10173
+ const legacyCfg = join18(process.cwd(), ".sechroom.json");
8126
10174
  if (existsSync14(legacyCfg)) {
8127
- rmSync4(legacyCfg, { force: true });
10175
+ rmSync5(legacyCfg, { force: true });
8128
10176
  removed.push(legacyCfg);
8129
10177
  }
8130
- const legacySem = join17(process.cwd(), ".sem");
10178
+ const legacySem = join18(process.cwd(), ".sem");
8131
10179
  if (existsSync14(legacySem)) {
8132
- rmSync4(legacySem, { force: true });
10180
+ rmSync5(legacySem, { force: true });
8133
10181
  removed.push(legacySem);
8134
10182
  }
8135
10183
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -8154,8 +10202,8 @@ function registerReset(program2) {
8154
10202
  }
8155
10203
 
8156
10204
  // src/commands/skills.ts
8157
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
8158
- import { join as join18 } from "path";
10205
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, statSync as statSync4, writeFileSync as writeFileSync14 } from "fs";
10206
+ import { join as join19 } from "path";
8159
10207
  function filenameFromDisposition(header) {
8160
10208
  if (!header) return void 0;
8161
10209
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -8163,11 +10211,11 @@ function filenameFromDisposition(header) {
8163
10211
  }
8164
10212
  function resolveOutputPath(output, serverFilename) {
8165
10213
  const filename = serverFilename || "skills.zip";
8166
- if (!output) return join18(process.cwd(), filename);
10214
+ if (!output) return join19(process.cwd(), filename);
8167
10215
  const looksLikeDir = output.endsWith("/") || existsSync15(output) && statSync4(output).isDirectory();
8168
10216
  if (looksLikeDir) {
8169
- mkdirSync14(output, { recursive: true });
8170
- return join18(output, filename);
10217
+ mkdirSync15(output, { recursive: true });
10218
+ return join19(output, filename);
8171
10219
  }
8172
10220
  return output;
8173
10221
  }
@@ -8198,7 +10246,7 @@ async function downloadZip(label, call, output) {
8198
10246
  const buf = Buffer.from(res.data);
8199
10247
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
8200
10248
  const path = resolveOutputPath(output, filename);
8201
- writeFileSync13(path, buf);
10249
+ writeFileSync14(path, buf);
8202
10250
  return { path, bytes: buf.length, filename };
8203
10251
  }
8204
10252
  function registerSkills(program2) {
@@ -8373,8 +10421,8 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
8373
10421
 
8374
10422
  // src/commands/sweep.ts
8375
10423
  import { existsSync as existsSync16 } from "fs";
8376
- import { dirname as dirname12, join as join19, resolve as resolve4 } from "path";
8377
- var DEFAULT_MANIFEST = join19(".sechroom", "repos.json");
10424
+ import { dirname as dirname13, join as join20, resolve as resolve5 } from "path";
10425
+ var DEFAULT_MANIFEST = join20(".sechroom", "repos.json");
8378
10426
  function planEntry(entry, root) {
8379
10427
  const dir = resolveChildDir(entry.path, root);
8380
10428
  if (!existsSync16(dir)) {
@@ -8437,7 +10485,7 @@ Examples:
8437
10485
  const g = cmd.optsWithGlobals();
8438
10486
  const json = Boolean(g.json);
8439
10487
  const dryRun = Boolean(opts.dryRun);
8440
- const manifestPath = resolve4(opts.manifest);
10488
+ const manifestPath = resolve5(opts.manifest);
8441
10489
  let repos;
8442
10490
  try {
8443
10491
  repos = readManifest(manifestPath);
@@ -8453,7 +10501,7 @@ Examples:
8453
10501
  `);
8454
10502
  return;
8455
10503
  }
8456
- const root = dirname12(dirname12(manifestPath));
10504
+ const root = dirname13(dirname13(manifestPath));
8457
10505
  const plans = repos.map((entry) => planEntry(entry, root));
8458
10506
  if (!json) {
8459
10507
  process.stderr.write(
@@ -8873,7 +10921,7 @@ async function readStdin4() {
8873
10921
  function resolveVersion() {
8874
10922
  try {
8875
10923
  const pkg = JSON.parse(
8876
- readFileSync13(new URL("../package.json", import.meta.url), "utf8")
10924
+ readFileSync15(new URL("../package.json", import.meta.url), "utf8")
8877
10925
  );
8878
10926
  return pkg.version ?? "0.0.0";
8879
10927
  } catch {