@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.
- package/dist/index.js +2554 -506
- 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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}", {
|
|
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(
|
|
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", {
|
|
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
|
|
1546
|
-
import { dirname as
|
|
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
|
|
1556
|
-
import { dirname as
|
|
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
|
|
1710
|
-
appendFileSync(target.path, `${
|
|
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
|
|
1743
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
1722
1744
|
|
|
1723
|
-
// src/executor-run/
|
|
1724
|
-
var
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
1842
|
+
const parsed = {
|
|
1808
1843
|
usedPercent,
|
|
1809
|
-
windowMinutes: numberOf(
|
|
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 =
|
|
1977
|
-
const last =
|
|
1978
|
-
const total =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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(
|
|
2009
|
-
|
|
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")
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
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 {
|
|
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(
|
|
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(
|
|
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((
|
|
2267
|
-
|
|
2268
|
-
|
|
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(
|
|
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))
|
|
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((
|
|
2739
|
+
return new Promise((resolve6) => {
|
|
2311
2740
|
this.child?.once(
|
|
2312
2741
|
"exit",
|
|
2313
|
-
(code) =>
|
|
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(
|
|
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((
|
|
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) =>
|
|
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 {
|
|
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(
|
|
2417
|
-
|
|
2418
|
-
|
|
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)
|
|
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(
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
`
|
|
3028
|
+
`API reachable again after ${consecutiveTransient} interruption(s) \u2014 resuming`
|
|
2604
3029
|
);
|
|
2605
|
-
|
|
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
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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
|
-
`
|
|
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 {
|
|
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
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
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(`[${
|
|
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("
|
|
2800
|
-
|
|
2801
|
-
|
|
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
|
-
|
|
2804
|
-
|
|
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)
|
|
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
|
|
2830
|
-
readFileSync as
|
|
2831
|
-
rmSync as
|
|
2832
|
-
writeFileSync as
|
|
4257
|
+
mkdirSync as mkdirSync8,
|
|
4258
|
+
readFileSync as readFileSync6,
|
|
4259
|
+
rmSync as rmSync4,
|
|
4260
|
+
writeFileSync as writeFileSync7
|
|
2833
4261
|
} from "fs";
|
|
2834
|
-
import {
|
|
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
|
|
2838
|
-
import { delimiter, dirname as
|
|
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
|
|
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
|
|
4276
|
+
return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2848
4277
|
case "win32":
|
|
2849
|
-
return
|
|
4278
|
+
return join7(process.env.APPDATA ?? join7(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
2850
4279
|
default:
|
|
2851
|
-
return
|
|
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 ??
|
|
2857
|
-
const codexHome = opts.codexHome ??
|
|
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:
|
|
2863
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
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:
|
|
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:
|
|
2875
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
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:
|
|
2881
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
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:
|
|
2893
|
-
instruction: { surfaceKey: "antigravity", path:
|
|
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(
|
|
4332
|
+
if (existsSync5(dirname5(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
2904
4333
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
2905
|
-
if (existsSync5(
|
|
2906
|
-
if (existsSync5(
|
|
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 =
|
|
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) &&
|
|
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
|
-
|
|
2961
|
-
|
|
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(
|
|
4395
|
+
return installHooksJson(join8(claudeDir, "settings.json"), commands, dryRun);
|
|
2967
4396
|
}
|
|
2968
4397
|
function installCodexCommands(codexHome, commands, dryRun) {
|
|
2969
4398
|
return [
|
|
2970
|
-
installHooksJson(
|
|
2971
|
-
installCodexFeatureFlag(
|
|
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 ?
|
|
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
|
-
|
|
3001
|
-
|
|
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 =
|
|
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(
|
|
3030
|
-
const featureFlag = installCodexFeatureFlag(
|
|
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(
|
|
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 =
|
|
3151
|
-
|
|
3152
|
-
const path =
|
|
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
|
-
|
|
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 =
|
|
4601
|
+
const path = join9(process.cwd(), ".sechroom", BINDING_FILE);
|
|
3173
4602
|
const existed = existsSync7(path);
|
|
3174
|
-
if (existed)
|
|
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
|
|
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 =
|
|
4719
|
+
const path = join9(dir, ".sechroom", BINDING_FILE);
|
|
3282
4720
|
if (existsSync7(path)) {
|
|
3283
4721
|
try {
|
|
3284
4722
|
const b = JSON.parse(
|
|
3285
|
-
|
|
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 =
|
|
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
|
|
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
|
-
).
|
|
3418
|
-
|
|
3419
|
-
|
|
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
|
-
|
|
3436
|
-
|
|
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(
|
|
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
|
|
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
|
|
3528
|
-
const
|
|
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
|
|
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((
|
|
3571
|
-
wake =
|
|
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
|
-
() =>
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
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 () =>
|
|
3619
|
-
|
|
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
|
-
|
|
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((
|
|
3669
|
-
setTimeout(
|
|
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
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
3920
|
-
const statePath =
|
|
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
|
-
|
|
3937
|
-
|
|
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
|
-
) : [
|
|
3943
|
-
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
4245
|
-
sem ?
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4448
|
-
|
|
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((
|
|
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 =
|
|
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((
|
|
6519
|
+
return new Promise((resolve6) => {
|
|
4614
6520
|
const stop = () => {
|
|
4615
|
-
void conn.stop().finally(
|
|
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
|
|
4742
|
-
import { dirname as
|
|
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
|
|
4746
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4747
|
-
import { dirname as
|
|
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 =
|
|
6677
|
+
var INTENT_FILE = join13(".sechroom", "continuity.json");
|
|
4772
6678
|
function resolveIntentPath(start) {
|
|
4773
6679
|
let dir = start;
|
|
4774
6680
|
for (; ; ) {
|
|
4775
|
-
const candidate =
|
|
6681
|
+
const candidate = join13(dir, INTENT_FILE);
|
|
4776
6682
|
if (existsSync10(candidate)) return candidate;
|
|
4777
|
-
const parent =
|
|
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(
|
|
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 ?
|
|
4829
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) ??
|
|
7047
|
+
const path = resolveIntentPath(cwd) ?? join14(cwd, INTENT_FILE);
|
|
5142
7048
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
5143
|
-
|
|
5144
|
-
|
|
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
|
|
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 ?
|
|
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)").
|
|
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:
|
|
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:
|
|
7938
|
+
title: title ?? null,
|
|
5983
7939
|
tags: opts.tag ?? null,
|
|
5984
|
-
owner: unfiled ? null : {
|
|
7940
|
+
owner: unfiled ? null : {
|
|
7941
|
+
type: opts.ownerType,
|
|
7942
|
+
id: String(opts.ownerId ?? "")
|
|
7943
|
+
}
|
|
5985
7944
|
}
|
|
5986
7945
|
});
|
|
5987
7946
|
});
|
|
5988
|
-
const titlePart =
|
|
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(
|
|
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}", {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
6082
|
-
|
|
6083
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
8137
|
+
emitAction(
|
|
8138
|
+
`archived ${style.bold(memoryId)}`,
|
|
8139
|
+
data,
|
|
8140
|
+
cmd.optsWithGlobals().json
|
|
8141
|
+
);
|
|
6123
8142
|
});
|
|
6124
|
-
memory.command("restore <memoryId>").description(
|
|
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(
|
|
8154
|
+
emitAction(
|
|
8155
|
+
`restored ${style.bold(memoryId)}`,
|
|
8156
|
+
data,
|
|
8157
|
+
cmd.optsWithGlobals().json
|
|
8158
|
+
);
|
|
6134
8159
|
});
|
|
6135
|
-
memory.command("move <memoryId>").description(
|
|
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(
|
|
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", {
|
|
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(
|
|
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(
|
|
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
|
|
6264
|
-
import { mkdirSync as
|
|
6265
|
-
import { dirname as
|
|
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
|
|
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
|
-
|
|
8366
|
+
mkdirSync13(dirname12(path), { recursive: true });
|
|
6319
8367
|
}
|
|
6320
8368
|
function readOr(path, fallback) {
|
|
6321
8369
|
try {
|
|
6322
|
-
return
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6532
|
-
import { join as
|
|
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
|
-
|
|
6649
|
-
|
|
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
|
-
|
|
8705
|
+
mkdirSync14(aDir, { recursive: true });
|
|
6658
8706
|
const written = [];
|
|
6659
8707
|
for (const a of agents) {
|
|
6660
8708
|
const file = `${a.name}.md`;
|
|
6661
|
-
|
|
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
|
|
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
|
|
7050
|
-
import { isAbsolute, join as
|
|
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 :
|
|
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 =
|
|
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(
|
|
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(
|
|
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 ??
|
|
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:
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
8059
|
-
import { join as
|
|
8060
|
-
import { existsSync as existsSync14, readFileSync as
|
|
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 = () =>
|
|
8063
|
-
var globalSkillsDir = () =>
|
|
8064
|
-
var localAgentsDir = () =>
|
|
8065
|
-
var globalAgentsDir = () =>
|
|
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 =
|
|
10116
|
+
const lockPath = join18(dir, SKILLS_LOCK2);
|
|
8069
10117
|
if (!existsSync14(lockPath)) return removed;
|
|
8070
10118
|
try {
|
|
8071
|
-
const lock = JSON.parse(
|
|
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 =
|
|
10122
|
+
const p = join18(dir, name);
|
|
8075
10123
|
if (existsSync14(p)) {
|
|
8076
|
-
|
|
10124
|
+
rmSync5(p, { recursive: true, force: true });
|
|
8077
10125
|
removed.push(p);
|
|
8078
10126
|
}
|
|
8079
10127
|
}
|
|
8080
10128
|
}
|
|
8081
10129
|
} catch {
|
|
8082
10130
|
}
|
|
8083
|
-
|
|
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 =
|
|
10168
|
+
const stateDir = join18(process.cwd(), ".sechroom");
|
|
8121
10169
|
if (existsSync14(stateDir)) {
|
|
8122
|
-
|
|
10170
|
+
rmSync5(stateDir, { recursive: true, force: true });
|
|
8123
10171
|
removed.push(stateDir);
|
|
8124
10172
|
}
|
|
8125
|
-
const legacyCfg =
|
|
10173
|
+
const legacyCfg = join18(process.cwd(), ".sechroom.json");
|
|
8126
10174
|
if (existsSync14(legacyCfg)) {
|
|
8127
|
-
|
|
10175
|
+
rmSync5(legacyCfg, { force: true });
|
|
8128
10176
|
removed.push(legacyCfg);
|
|
8129
10177
|
}
|
|
8130
|
-
const legacySem =
|
|
10178
|
+
const legacySem = join18(process.cwd(), ".sem");
|
|
8131
10179
|
if (existsSync14(legacySem)) {
|
|
8132
|
-
|
|
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
|
|
8158
|
-
import { join as
|
|
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
|
|
10214
|
+
if (!output) return join19(process.cwd(), filename);
|
|
8167
10215
|
const looksLikeDir = output.endsWith("/") || existsSync15(output) && statSync4(output).isDirectory();
|
|
8168
10216
|
if (looksLikeDir) {
|
|
8169
|
-
|
|
8170
|
-
return
|
|
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
|
-
|
|
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
|
|
8377
|
-
var DEFAULT_MANIFEST =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
10924
|
+
readFileSync15(new URL("../package.json", import.meta.url), "utf8")
|
|
8877
10925
|
);
|
|
8878
10926
|
return pkg.version ?? "0.0.0";
|
|
8879
10927
|
} catch {
|