@sechroom/cli 2026.8.9-rc.6433a3deb → 2026.9.1
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 +652 -277
- 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 readFileSync20 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -702,17 +702,17 @@ function formatFailureMessage(error) {
|
|
|
702
702
|
} else if (isRecord(error)) {
|
|
703
703
|
const problem = error;
|
|
704
704
|
const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
|
|
705
|
-
const
|
|
705
|
+
const problemDetail2 = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
|
|
706
706
|
const fieldErrors = formatProblemErrors(problem.errors);
|
|
707
707
|
const structuredErrors = fieldErrors.length === 0 ? formatStructuredViolations(problem.violations) : [];
|
|
708
708
|
const parts = [
|
|
709
709
|
...title ? [title] : [],
|
|
710
|
-
...
|
|
710
|
+
...problemDetail2 ? [problemDetail2] : [],
|
|
711
711
|
...fieldErrors,
|
|
712
712
|
...structuredErrors
|
|
713
713
|
];
|
|
714
714
|
if (parts.length > 0) {
|
|
715
|
-
if (title && !
|
|
715
|
+
if (title && !problemDetail2 && fieldErrors.length === 0 && structuredErrors.length === 0) {
|
|
716
716
|
parts.push("No additional error detail was returned by the API.");
|
|
717
717
|
}
|
|
718
718
|
msg = parts.join("\n");
|
|
@@ -1001,8 +1001,8 @@ function recordMaterialisedSkills(dir, slug2, skills, meta = {}) {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
1003
|
// src/setup/materialise.ts
|
|
1004
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as
|
|
1005
|
-
import { join as
|
|
1004
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1005
|
+
import { join as join5 } from "path";
|
|
1006
1006
|
|
|
1007
1007
|
// src/setup/config-dirs.ts
|
|
1008
1008
|
import { homedir as homedir2 } from "os";
|
|
@@ -1371,6 +1371,89 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1371
1371
|
return resolveReferences(rows.systemRows, rows.personalRows, surface);
|
|
1372
1372
|
}
|
|
1373
1373
|
|
|
1374
|
+
// src/setup/skill-orphans.ts
|
|
1375
|
+
import { readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, statSync } from "fs";
|
|
1376
|
+
import { join as join4 } from "path";
|
|
1377
|
+
var INSTALL_MARKER = "<!-- sechroom-install:";
|
|
1378
|
+
var MAX_MARKER_SCAN_BYTES = 2 * 1024 * 1024;
|
|
1379
|
+
function bundleFromBody(body) {
|
|
1380
|
+
const start = body.indexOf(INSTALL_MARKER);
|
|
1381
|
+
if (start < 0) return null;
|
|
1382
|
+
const from = start + INSTALL_MARKER.length;
|
|
1383
|
+
const end = body.indexOf("-->", from);
|
|
1384
|
+
if (end < 0) return null;
|
|
1385
|
+
try {
|
|
1386
|
+
const payload = JSON.parse(body.slice(from, end).replaceAll("\\", "").trim());
|
|
1387
|
+
return typeof payload.Bundle === "string" && payload.Bundle ? payload.Bundle : null;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function readBody(path) {
|
|
1393
|
+
try {
|
|
1394
|
+
if (statSync(path).size > MAX_MARKER_SCAN_BYTES) return null;
|
|
1395
|
+
return readFileSync3(path, "utf8");
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
var SKILL_LAYOUT = {
|
|
1401
|
+
scan: (dir) => readEntries(dir).filter((e) => e.isDirectory()).map((e) => ({ name: e.name, bodyPath: join4(dir, e.name, "SKILL.md") }))
|
|
1402
|
+
};
|
|
1403
|
+
var AGENT_LAYOUT = {
|
|
1404
|
+
scan: (dir) => readEntries(dir).filter((e) => e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".toml"))).map((e) => ({ name: e.name, bodyPath: join4(dir, e.name) }))
|
|
1405
|
+
};
|
|
1406
|
+
function readEntries(dir) {
|
|
1407
|
+
try {
|
|
1408
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
1409
|
+
} catch {
|
|
1410
|
+
return [];
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function scanMarked(dir, layout) {
|
|
1414
|
+
const marked = [];
|
|
1415
|
+
for (const item of layout.scan(dir)) {
|
|
1416
|
+
const body = readBody(item.bodyPath);
|
|
1417
|
+
if (body === null || !body.includes(INSTALL_MARKER)) continue;
|
|
1418
|
+
marked.push({ name: item.name, bundle: bundleFromBody(body) });
|
|
1419
|
+
}
|
|
1420
|
+
return marked;
|
|
1421
|
+
}
|
|
1422
|
+
function markedItems(dir, layout) {
|
|
1423
|
+
return scanMarked(dir, layout).map((item) => item.name);
|
|
1424
|
+
}
|
|
1425
|
+
function lockedNames(lock, exceptSlug) {
|
|
1426
|
+
const names = /* @__PURE__ */ new Set();
|
|
1427
|
+
for (const [slug2, entry] of Object.entries(lock)) {
|
|
1428
|
+
if (slug2 === exceptSlug) continue;
|
|
1429
|
+
for (const name of entry.skills ?? []) names.add(name);
|
|
1430
|
+
}
|
|
1431
|
+
return names;
|
|
1432
|
+
}
|
|
1433
|
+
function orphansAfterInstall(dir, layout, slug2, keep, ownBundles2 = /* @__PURE__ */ new Set()) {
|
|
1434
|
+
if (keep.length === 0) return [];
|
|
1435
|
+
const lock = readSkillsLock(dir);
|
|
1436
|
+
const protectedNames = /* @__PURE__ */ new Set([...keep, ...lockedNames(lock, slug2)]);
|
|
1437
|
+
const mine = scanMarked(dir, layout).filter((item) => ownBundles2.size === 0 || item.bundle === null || ownBundles2.has(item.bundle)).map((item) => item.name);
|
|
1438
|
+
const managed = /* @__PURE__ */ new Set([...lock[slug2]?.skills ?? [], ...mine]);
|
|
1439
|
+
return [...managed].filter((name) => !protectedNames.has(name)).sort();
|
|
1440
|
+
}
|
|
1441
|
+
function orphansAfterClean(dir, layout) {
|
|
1442
|
+
const protectedNames = lockedNames(readSkillsLock(dir));
|
|
1443
|
+
return markedItems(dir, layout).filter((name) => !protectedNames.has(name)).sort();
|
|
1444
|
+
}
|
|
1445
|
+
function pruneItems(dir, names) {
|
|
1446
|
+
const removed = [];
|
|
1447
|
+
for (const name of names) {
|
|
1448
|
+
try {
|
|
1449
|
+
rmSync2(join4(dir, name), { recursive: true, force: true });
|
|
1450
|
+
removed.push(name);
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return removed;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1374
1457
|
// src/setup/materialise.ts
|
|
1375
1458
|
var CLIENT_SURFACE = {
|
|
1376
1459
|
claude: "claude-code",
|
|
@@ -1379,8 +1462,8 @@ var CLIENT_SURFACE = {
|
|
|
1379
1462
|
function writeSkills(dir, skills, surface) {
|
|
1380
1463
|
const written = [];
|
|
1381
1464
|
for (const s of skills) {
|
|
1382
|
-
mkdirSync3(
|
|
1383
|
-
writeFileSync3(
|
|
1465
|
+
mkdirSync3(join5(dir, s.name), { recursive: true });
|
|
1466
|
+
writeFileSync3(join5(dir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
1384
1467
|
written.push(s.name);
|
|
1385
1468
|
}
|
|
1386
1469
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1430,14 +1513,17 @@ function codexAgentToml(agent) {
|
|
|
1430
1513
|
""
|
|
1431
1514
|
].join("\n");
|
|
1432
1515
|
}
|
|
1516
|
+
function agentFileName(name, surface) {
|
|
1517
|
+
return `${name}.${surface === CLIENT_SURFACE.codex ? "toml" : "md"}`;
|
|
1518
|
+
}
|
|
1433
1519
|
function writeAgents(dir, agents, surface) {
|
|
1434
1520
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1435
1521
|
const written = [];
|
|
1436
1522
|
for (const a of agents) {
|
|
1437
1523
|
const codex = surface === CLIENT_SURFACE.codex;
|
|
1438
|
-
const file =
|
|
1524
|
+
const file = agentFileName(a.name, surface);
|
|
1439
1525
|
const body = codex ? codexAgentToml(a) : a.body.endsWith("\n") ? a.body : a.body + "\n";
|
|
1440
|
-
writeFileSync3(
|
|
1526
|
+
writeFileSync3(join5(dir, file), body);
|
|
1441
1527
|
written.push(file);
|
|
1442
1528
|
}
|
|
1443
1529
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1446,10 +1532,10 @@ function writeAgents(dir, agents, surface) {
|
|
|
1446
1532
|
function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
1447
1533
|
if (!refs.length || !skills.length) return [];
|
|
1448
1534
|
for (const s of skills) {
|
|
1449
|
-
const refDir =
|
|
1535
|
+
const refDir = join5(dir, s.name, "references");
|
|
1450
1536
|
mkdirSync3(refDir, { recursive: true });
|
|
1451
1537
|
for (const r of refs) {
|
|
1452
|
-
writeFileSync3(
|
|
1538
|
+
writeFileSync3(join5(refDir, `${r.name}.md`), r.body.endsWith("\n") ? r.body : r.body + "\n");
|
|
1453
1539
|
}
|
|
1454
1540
|
}
|
|
1455
1541
|
return refs.map((r) => r.name);
|
|
@@ -1459,6 +1545,8 @@ var SKILL_SPEC = {
|
|
|
1459
1545
|
dir: skillsDir,
|
|
1460
1546
|
resolve: resolveSkillSet,
|
|
1461
1547
|
write: writeSkills,
|
|
1548
|
+
lockNames: (items) => items.map((i) => i.name),
|
|
1549
|
+
layout: SKILL_LAYOUT,
|
|
1462
1550
|
supportsCodex: true
|
|
1463
1551
|
};
|
|
1464
1552
|
var AGENT_SPEC = {
|
|
@@ -1466,8 +1554,18 @@ var AGENT_SPEC = {
|
|
|
1466
1554
|
dir: agentsDir,
|
|
1467
1555
|
resolve: resolveAgentSet,
|
|
1468
1556
|
write: writeAgents,
|
|
1557
|
+
lockNames: (items, surface) => items.map((i) => agentFileName(i.name, surface)),
|
|
1558
|
+
layout: AGENT_LAYOUT,
|
|
1469
1559
|
supportsCodex: true
|
|
1470
1560
|
};
|
|
1561
|
+
function ownBundles(items) {
|
|
1562
|
+
const bundles = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const item of items) {
|
|
1564
|
+
const bundle = bundleFromBody(item.body);
|
|
1565
|
+
if (bundle) bundles.add(bundle);
|
|
1566
|
+
}
|
|
1567
|
+
return bundles;
|
|
1568
|
+
}
|
|
1471
1569
|
function scopeOf(opts) {
|
|
1472
1570
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1473
1571
|
}
|
|
@@ -1547,8 +1645,16 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1547
1645
|
validateCodexWorkerDependencies(items, resolveAgentSet(rows, t.surface));
|
|
1548
1646
|
}
|
|
1549
1647
|
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1550
|
-
const
|
|
1648
|
+
const orphans = orphansAfterInstall(
|
|
1649
|
+
t.dir,
|
|
1650
|
+
spec.layout,
|
|
1651
|
+
DEFAULT_SKILLS_SLUG,
|
|
1652
|
+
spec.lockNames(items, t.surface),
|
|
1653
|
+
ownBundles(items)
|
|
1654
|
+
);
|
|
1655
|
+
const written = dryRun ? spec.lockNames(items, t.surface) : spec.write(t.dir, items, t.surface);
|
|
1551
1656
|
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1657
|
+
const pruned = dryRun ? orphans : pruneItems(t.dir, orphans);
|
|
1552
1658
|
return {
|
|
1553
1659
|
client: t.client,
|
|
1554
1660
|
surface: t.surface,
|
|
@@ -1559,7 +1665,8 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1559
1665
|
items: items.map(({ name, source }) => ({ name, source })),
|
|
1560
1666
|
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1561
1667
|
written,
|
|
1562
|
-
refsWritten
|
|
1668
|
+
refsWritten,
|
|
1669
|
+
pruned
|
|
1563
1670
|
};
|
|
1564
1671
|
});
|
|
1565
1672
|
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
@@ -1575,6 +1682,12 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1575
1682
|
console.log(
|
|
1576
1683
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1577
1684
|
);
|
|
1685
|
+
if (r.pruned.length) {
|
|
1686
|
+
console.log(
|
|
1687
|
+
style.yellow("! ") + `${dryRun ? "would prune" : "pruned"} ${r.pruned.length} superseded ${spec.kind}(s) no longer in the bundle ${style.dim("\u2192")} ${r.dir}`
|
|
1688
|
+
);
|
|
1689
|
+
for (const name of r.pruned) console.log(` ${style.yellow("-")} ${name}`);
|
|
1690
|
+
}
|
|
1578
1691
|
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1579
1692
|
}
|
|
1580
1693
|
}
|
|
@@ -1598,7 +1711,7 @@ function runList(spec, cmd, opts) {
|
|
|
1598
1711
|
const out = targets.map((t) => {
|
|
1599
1712
|
const lock = readSkillsLock(t.dir);
|
|
1600
1713
|
const entries = Object.entries(lock).flatMap(
|
|
1601
|
-
([slug2, e]) => (e.skills ?? []).map((name) => ({ slug: slug2, name, present: existsSync3(
|
|
1714
|
+
([slug2, e]) => (e.skills ?? []).map((name) => ({ slug: slug2, name, present: existsSync3(join5(t.dir, name)) }))
|
|
1602
1715
|
);
|
|
1603
1716
|
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1604
1717
|
});
|
|
@@ -1633,33 +1746,47 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1633
1746
|
} catch (err2) {
|
|
1634
1747
|
return fail(err2.message);
|
|
1635
1748
|
}
|
|
1749
|
+
const pruneOrphans = Boolean(opts.pruneOrphans);
|
|
1636
1750
|
const cleaned = [];
|
|
1637
1751
|
const missing = [];
|
|
1638
1752
|
for (const t of targets) {
|
|
1639
1753
|
const lock = readSkillsLock(t.dir);
|
|
1640
1754
|
const entry = lock[slug2];
|
|
1641
|
-
if (!entry) {
|
|
1642
|
-
missing.push(
|
|
1755
|
+
if (!entry && !pruneOrphans) {
|
|
1756
|
+
missing.push(join5(t.dir, SKILLS_LOCK));
|
|
1643
1757
|
continue;
|
|
1644
1758
|
}
|
|
1645
1759
|
const removed = [];
|
|
1646
|
-
for (const name of entry
|
|
1647
|
-
const p =
|
|
1760
|
+
for (const name of entry?.skills ?? []) {
|
|
1761
|
+
const p = join5(t.dir, name);
|
|
1648
1762
|
if (existsSync3(p)) {
|
|
1649
|
-
|
|
1763
|
+
rmSync3(p, { recursive: true, force: true });
|
|
1650
1764
|
removed.push(name);
|
|
1651
1765
|
}
|
|
1652
1766
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1767
|
+
if (entry) {
|
|
1768
|
+
delete lock[slug2];
|
|
1769
|
+
writeSkillsLock(t.dir, lock);
|
|
1770
|
+
}
|
|
1771
|
+
const pruned = pruneOrphans ? pruneItems(t.dir, orphansAfterClean(t.dir, spec.layout)) : [];
|
|
1772
|
+
if (!entry && pruned.length === 0) {
|
|
1773
|
+
missing.push(join5(t.dir, SKILLS_LOCK));
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed, pruned });
|
|
1656
1777
|
}
|
|
1657
1778
|
if (cleaned.length === 0) {
|
|
1658
1779
|
return fail(`No materialised ${spec.kind}s recorded for '${slug2}' in ${missing.join(", ")}.`);
|
|
1659
1780
|
}
|
|
1660
|
-
if (json) return emit({ kind: spec.kind, client: selection, slug: slug2, cleaned, missing }, true);
|
|
1781
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug: slug2, pruneOrphans, cleaned, missing }, true);
|
|
1661
1782
|
for (const c of cleaned) {
|
|
1662
1783
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug2} from ${c.dir}`));
|
|
1784
|
+
if (c.pruned.length) {
|
|
1785
|
+
console.log(
|
|
1786
|
+
style.yellow("! ") + `pruned ${c.pruned.length} orphaned ${spec.kind}(s) with no lock entry from ${c.dir}`
|
|
1787
|
+
);
|
|
1788
|
+
for (const name of c.pruned) console.log(` ${style.yellow("-")} ${name}`);
|
|
1789
|
+
}
|
|
1663
1790
|
}
|
|
1664
1791
|
}
|
|
1665
1792
|
|
|
@@ -1683,12 +1810,12 @@ target:gpt-codex-agent), the dispatchable workers your loop skills call
|
|
|
1683
1810
|
);
|
|
1684
1811
|
agents.command("install").description("Materialise your installed subagents to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--dry-run", "print what would be written; write nothing").option("--client <client>", "claude, codex, or all").option("--json", "machine output").action((opts, cmd) => runInstall(AGENT_SPEC, cmd, opts));
|
|
1685
1812
|
agents.command("list").description("List the subagents materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").option("--client <client>", "claude, codex, or all").action((opts, cmd) => runList(AGENT_SPEC, cmd, opts));
|
|
1686
|
-
agents.command("clean [slug]").description(`Remove subagent files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").option("--client <client>", "claude, codex, or all").action((slugArg, opts, cmd) => runClean(AGENT_SPEC, cmd, opts, slugArg));
|
|
1813
|
+
agents.command("clean [slug]").description(`Remove subagent files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--prune-orphans", "also remove sechroom-installed agent files that no lock entry claims (renamed/removed upstream)").option("--json", "machine output").option("--client <client>", "claude, codex, or all").action((slugArg, opts, cmd) => runClean(AGENT_SPEC, cmd, opts, slugArg));
|
|
1687
1814
|
}
|
|
1688
1815
|
|
|
1689
1816
|
// src/commands/channel.ts
|
|
1690
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as
|
|
1691
|
-
import { dirname as dirname9, join as
|
|
1817
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
1818
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
1692
1819
|
import {
|
|
1693
1820
|
HttpTransportType,
|
|
1694
1821
|
HubConnectionBuilder
|
|
@@ -1872,6 +1999,44 @@ async function runDriverLoop(ports, options) {
|
|
|
1872
1999
|
}
|
|
1873
2000
|
throw error;
|
|
1874
2001
|
}
|
|
2002
|
+
if (task.reviewExecution) {
|
|
2003
|
+
const binding = ports.verifyTaskBinding ? await ports.verifyTaskBinding(task) : {
|
|
2004
|
+
ok: false,
|
|
2005
|
+
reason: "review-execution task has no driver-side target verifier; refusing before turn"
|
|
2006
|
+
};
|
|
2007
|
+
if (!binding.ok) {
|
|
2008
|
+
const evidence = binding.evidence ? `
|
|
2009
|
+
|
|
2010
|
+
Observed binding:
|
|
2011
|
+
${binding.evidence}` : "";
|
|
2012
|
+
const text2 = `Review target preflight refused before turn. ${binding.reason ?? "repository/head mismatch"}${evidence}`;
|
|
2013
|
+
ports.log(
|
|
2014
|
+
`REVIEW TARGET REFUSED ${claim.memoryId}: ${binding.reason ?? "mismatch"}`
|
|
2015
|
+
);
|
|
2016
|
+
try {
|
|
2017
|
+
const done = await ports.completeLease(
|
|
2018
|
+
claim,
|
|
2019
|
+
"blocked",
|
|
2020
|
+
text2,
|
|
2021
|
+
`${task.title} \u2014 review target preflight refused`
|
|
2022
|
+
);
|
|
2023
|
+
summary.completed++;
|
|
2024
|
+
ports.log(
|
|
2025
|
+
`completed ${claim.memoryId} verdict:blocked \u2192 ${done.completionMemoryId ?? done.outcome}`
|
|
2026
|
+
);
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
if (e instanceof AuthExpiredError) throw e;
|
|
2029
|
+
summary.abandoned++;
|
|
2030
|
+
ports.log(
|
|
2031
|
+
`COMPLETE REJECTED for ${claim.memoryId} (${String(e)}) \u2014 refused review target will re-offer; investigate the lease gap.`
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
if (options.once) break;
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (binding.evidence)
|
|
2038
|
+
task = { ...task, reviewTargetEvidence: binding.evidence };
|
|
2039
|
+
}
|
|
1875
2040
|
const stopHeartbeat = ports.startLeaseHeartbeat(claim);
|
|
1876
2041
|
let result;
|
|
1877
2042
|
try {
|
|
@@ -1956,15 +2121,19 @@ Delivery: FAILED unexpectedly (${String(e)}) \u2014 changes remain in the execut
|
|
|
1956
2121
|
return summary;
|
|
1957
2122
|
}
|
|
1958
2123
|
function closeoutText(task, result) {
|
|
2124
|
+
const targetEvidence = task.reviewTargetEvidence ? `
|
|
2125
|
+
|
|
2126
|
+
Review target preflight evidence:
|
|
2127
|
+
${task.reviewTargetEvidence}` : "";
|
|
1959
2128
|
if (!result.packet)
|
|
1960
2129
|
return `Driven codex run ended without a sechroom_closeout packet (soft-fail). Last agent message:
|
|
1961
2130
|
|
|
1962
|
-
${result.lastAgentMessage || "(none)"}`;
|
|
2131
|
+
${result.lastAgentMessage || "(none)"}${targetEvidence}`;
|
|
1963
2132
|
const evidence = result.packet.evidence?.length ? `
|
|
1964
2133
|
|
|
1965
2134
|
Evidence:
|
|
1966
2135
|
${result.packet.evidence.map((e) => `- ${e}`).join("\n")}` : "";
|
|
1967
|
-
return `${result.packet.summary}${evidence}
|
|
2136
|
+
return `${result.packet.summary}${evidence}${targetEvidence}
|
|
1968
2137
|
|
|
1969
2138
|
(terminal_status: ${result.packet.terminal_status}; driven by sechroom executor run.)`;
|
|
1970
2139
|
}
|
|
@@ -2006,7 +2175,8 @@ async function materializeClaimedTask(request, memoryId) {
|
|
|
2006
2175
|
}
|
|
2007
2176
|
return {
|
|
2008
2177
|
title: card.title ?? memoryId,
|
|
2009
|
-
text: assemblePrompt(card, components)
|
|
2178
|
+
text: assemblePrompt(card, components),
|
|
2179
|
+
reviewExecution: card.reviewExecution ?? void 0
|
|
2010
2180
|
};
|
|
2011
2181
|
}
|
|
2012
2182
|
function validatePackage(value, expectedSlug, expectedVersion) {
|
|
@@ -2040,6 +2210,11 @@ ${card.task.boundaries}`,
|
|
|
2040
2210
|
`## Closeout
|
|
2041
2211
|
${card.task.closeout}`
|
|
2042
2212
|
];
|
|
2213
|
+
if (card.reviewExecution)
|
|
2214
|
+
sections.push(
|
|
2215
|
+
`## Review target
|
|
2216
|
+
${renderReviewExecution(card.reviewExecution)}`
|
|
2217
|
+
);
|
|
2043
2218
|
if (components.length > 0)
|
|
2044
2219
|
sections.push(
|
|
2045
2220
|
`## Task context
|
|
@@ -2047,6 +2222,17 @@ ${components.map(renderComponent).join("\n\n")}`
|
|
|
2047
2222
|
);
|
|
2048
2223
|
return sections.join("\n\n");
|
|
2049
2224
|
}
|
|
2225
|
+
function renderReviewExecution(target) {
|
|
2226
|
+
return [
|
|
2227
|
+
`kind: ${target.kind}`,
|
|
2228
|
+
`round: ${target.round}`,
|
|
2229
|
+
`repository: ${target.repository}`,
|
|
2230
|
+
`base commit: ${target.baseCommit}`,
|
|
2231
|
+
`head commit: ${target.headCommit}`,
|
|
2232
|
+
target.preferredInstanceKey ? `preferred instance: ${target.preferredInstanceKey}` : void 0,
|
|
2233
|
+
target.preferredLaneId ? `preferred lane: ${target.preferredLaneId}` : void 0
|
|
2234
|
+
].filter((line) => line !== void 0).join("\n");
|
|
2235
|
+
}
|
|
2050
2236
|
function renderComponent(component) {
|
|
2051
2237
|
return [
|
|
2052
2238
|
`<!-- sechroom-task-context component=${JSON.stringify(component.slug)} id=${JSON.stringify(component.sourceId)} sourceVersion=${component.sourceVersion} -->`,
|
|
@@ -2057,21 +2243,22 @@ function renderComponent(component) {
|
|
|
2057
2243
|
|
|
2058
2244
|
// src/commands/executor.ts
|
|
2059
2245
|
import { execFileSync } from "child_process";
|
|
2060
|
-
import {
|
|
2061
|
-
import {
|
|
2246
|
+
import { randomUUID } from "crypto";
|
|
2247
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
2248
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
2062
2249
|
|
|
2063
2250
|
// src/sem.ts
|
|
2064
|
-
import { dirname as dirname2, join as
|
|
2065
|
-
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as
|
|
2066
|
-
var SEM_FILE =
|
|
2251
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
2252
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2253
|
+
var SEM_FILE = join6(".sechroom", "lane.json");
|
|
2067
2254
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
2068
2255
|
function localSemPath(cwd = process.cwd()) {
|
|
2069
|
-
return
|
|
2256
|
+
return join6(cwd, SEM_FILE);
|
|
2070
2257
|
}
|
|
2071
2258
|
function resolveSemPathForRead(start = process.cwd()) {
|
|
2072
2259
|
let dir = start;
|
|
2073
2260
|
while (true) {
|
|
2074
|
-
const candidate =
|
|
2261
|
+
const candidate = join6(dir, SEM_FILE);
|
|
2075
2262
|
if (existsSync4(candidate)) return candidate;
|
|
2076
2263
|
const parent = dirname2(dir);
|
|
2077
2264
|
if (parent === dir) return void 0;
|
|
@@ -2083,7 +2270,7 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
2083
2270
|
let dir = start;
|
|
2084
2271
|
let gitPath;
|
|
2085
2272
|
for (; ; ) {
|
|
2086
|
-
const candidate =
|
|
2273
|
+
const candidate = join6(dir, ".git");
|
|
2087
2274
|
if (existsSync4(candidate)) {
|
|
2088
2275
|
gitPath = candidate;
|
|
2089
2276
|
break;
|
|
@@ -2092,14 +2279,14 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
2092
2279
|
if (parent === dir) break;
|
|
2093
2280
|
dir = parent;
|
|
2094
2281
|
}
|
|
2095
|
-
if (!gitPath ||
|
|
2096
|
-
const gitFile =
|
|
2282
|
+
if (!gitPath || statSync2(gitPath).isDirectory()) return lane;
|
|
2283
|
+
const gitFile = readFileSync4(gitPath, "utf8");
|
|
2097
2284
|
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
2098
2285
|
if (!common) return lane;
|
|
2099
|
-
const worktreesDir =
|
|
2100
|
-
const siblings =
|
|
2286
|
+
const worktreesDir = join6(common[1], "worktrees");
|
|
2287
|
+
const siblings = readdirSync2(worktreesDir).filter((n) => {
|
|
2101
2288
|
try {
|
|
2102
|
-
return
|
|
2289
|
+
return statSync2(join6(worktreesDir, n)).isDirectory();
|
|
2103
2290
|
} catch {
|
|
2104
2291
|
return false;
|
|
2105
2292
|
}
|
|
@@ -2121,10 +2308,10 @@ function serializeSem(values) {
|
|
|
2121
2308
|
function readSem(path) {
|
|
2122
2309
|
const p = path ?? resolveSemPathForRead();
|
|
2123
2310
|
if (!p || !existsSync4(p)) return void 0;
|
|
2124
|
-
return { path: p, values: parseLaneJson(
|
|
2311
|
+
return { path: p, values: parseLaneJson(readFileSync4(p, "utf8")) };
|
|
2125
2312
|
}
|
|
2126
2313
|
function readLocalSemValues(cwd = process.cwd()) {
|
|
2127
|
-
const next =
|
|
2314
|
+
const next = join6(cwd, SEM_FILE);
|
|
2128
2315
|
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
2129
2316
|
return {};
|
|
2130
2317
|
}
|
|
@@ -2170,7 +2357,7 @@ var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
|
2170
2357
|
) + "\n";
|
|
2171
2358
|
function ensureContinuityScaffold(semPath) {
|
|
2172
2359
|
try {
|
|
2173
|
-
const target =
|
|
2360
|
+
const target = join6(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
2174
2361
|
if (existsSync4(target)) return;
|
|
2175
2362
|
writeFileSync4(target, CONTINUITY_SCAFFOLD);
|
|
2176
2363
|
} catch {
|
|
@@ -2185,7 +2372,7 @@ function ignoresSem(content) {
|
|
|
2185
2372
|
function inGitRepo(startDir) {
|
|
2186
2373
|
let dir = startDir;
|
|
2187
2374
|
for (; ; ) {
|
|
2188
|
-
if (existsSync4(
|
|
2375
|
+
if (existsSync4(join6(dir, ".git"))) return true;
|
|
2189
2376
|
const parent = dirname2(dir);
|
|
2190
2377
|
if (parent === dir) return false;
|
|
2191
2378
|
dir = parent;
|
|
@@ -2194,11 +2381,11 @@ function inGitRepo(startDir) {
|
|
|
2194
2381
|
function resolveGitignoreTarget(startDir) {
|
|
2195
2382
|
let dir = startDir;
|
|
2196
2383
|
for (; ; ) {
|
|
2197
|
-
const gi =
|
|
2384
|
+
const gi = join6(dir, ".gitignore");
|
|
2198
2385
|
if (existsSync4(gi)) return { path: gi, exists: true };
|
|
2199
2386
|
const parent = dirname2(dir);
|
|
2200
|
-
if (existsSync4(
|
|
2201
|
-
return { path:
|
|
2387
|
+
if (existsSync4(join6(dir, ".git")) || parent === dir) {
|
|
2388
|
+
return { path: join6(startDir, ".gitignore"), exists: false };
|
|
2202
2389
|
}
|
|
2203
2390
|
dir = parent;
|
|
2204
2391
|
}
|
|
@@ -2209,7 +2396,7 @@ function ensureSemIgnored(semPath) {
|
|
|
2209
2396
|
if (!inGitRepo(checkoutDir)) return;
|
|
2210
2397
|
const target = resolveGitignoreTarget(checkoutDir);
|
|
2211
2398
|
if (target.exists) {
|
|
2212
|
-
const content =
|
|
2399
|
+
const content = readFileSync4(target.path, "utf8");
|
|
2213
2400
|
if (ignoresSem(content)) return;
|
|
2214
2401
|
const sep2 = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2215
2402
|
appendFileSync(target.path, `${sep2}${STATE_DIR_IGNORE}
|
|
@@ -2223,7 +2410,7 @@ function ensureSemIgnored(semPath) {
|
|
|
2223
2410
|
}
|
|
2224
2411
|
|
|
2225
2412
|
// src/commands/executor-run.ts
|
|
2226
|
-
import { join as
|
|
2413
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
2227
2414
|
|
|
2228
2415
|
// src/executor-run/usage.ts
|
|
2229
2416
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
@@ -2842,7 +3029,9 @@ var CodexAppServer = class {
|
|
|
2842
3029
|
modelId,
|
|
2843
3030
|
executorInstanceId: this.options.executorInstanceId,
|
|
2844
3031
|
leaseId: telemetry?.leaseId ?? null,
|
|
2845
|
-
turnId: turnId || null
|
|
3032
|
+
turnId: turnId || null,
|
|
3033
|
+
originatingReviewId: telemetry?.originatingReviewId ?? null,
|
|
3034
|
+
reviewRound: telemetry?.reviewRound ?? null
|
|
2846
3035
|
});
|
|
2847
3036
|
this.onNotification = (msg) => {
|
|
2848
3037
|
if (msg.method === "thread/tokenUsage/updated") {
|
|
@@ -3366,8 +3555,8 @@ import {
|
|
|
3366
3555
|
closeSync,
|
|
3367
3556
|
mkdirSync as mkdirSync6,
|
|
3368
3557
|
openSync,
|
|
3369
|
-
readFileSync as
|
|
3370
|
-
rmSync as
|
|
3558
|
+
readFileSync as readFileSync5,
|
|
3559
|
+
rmSync as rmSync4,
|
|
3371
3560
|
writeFileSync as writeFileSync5
|
|
3372
3561
|
} from "fs";
|
|
3373
3562
|
import { readFile } from "fs/promises";
|
|
@@ -3376,7 +3565,7 @@ import {
|
|
|
3376
3565
|
createServer as createServer2
|
|
3377
3566
|
} from "net";
|
|
3378
3567
|
import { tmpdir } from "os";
|
|
3379
|
-
import { dirname as dirname4, join as
|
|
3568
|
+
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
3380
3569
|
var DEFAULT_RESTART_POLICY = {
|
|
3381
3570
|
enabled: true,
|
|
3382
3571
|
maxRetries: 10,
|
|
@@ -4233,7 +4422,7 @@ function isProcessAlive(pid) {
|
|
|
4233
4422
|
function readPidFile(path) {
|
|
4234
4423
|
let raw;
|
|
4235
4424
|
try {
|
|
4236
|
-
raw =
|
|
4425
|
+
raw = readFileSync5(path, "utf8");
|
|
4237
4426
|
} catch (error) {
|
|
4238
4427
|
if (error.code === "ENOENT") return void 0;
|
|
4239
4428
|
throw error;
|
|
@@ -4247,7 +4436,7 @@ function claimPidFile(path, options = {}) {
|
|
|
4247
4436
|
});
|
|
4248
4437
|
let raw;
|
|
4249
4438
|
try {
|
|
4250
|
-
raw =
|
|
4439
|
+
raw = readFileSync5(path, "utf8");
|
|
4251
4440
|
} catch (error) {
|
|
4252
4441
|
if (error.code === "ENOENT") return;
|
|
4253
4442
|
throw error;
|
|
@@ -4261,7 +4450,7 @@ function claimPidFile(path, options = {}) {
|
|
|
4261
4450
|
log(
|
|
4262
4451
|
`stale pid-file ${path} (${valid ? `pid ${pid}` : "unparseable"} not running) \u2014 reclaiming`
|
|
4263
4452
|
);
|
|
4264
|
-
|
|
4453
|
+
rmSync4(path, { force: true });
|
|
4265
4454
|
}
|
|
4266
4455
|
function writePidFile(path, pid) {
|
|
4267
4456
|
mkdirSync6(dirname4(path), { recursive: true });
|
|
@@ -4271,7 +4460,7 @@ function writePidFile(path, pid) {
|
|
|
4271
4460
|
function removePidFileIfOwned(path, pid, log) {
|
|
4272
4461
|
if (readPidFile(path) !== pid) return;
|
|
4273
4462
|
try {
|
|
4274
|
-
|
|
4463
|
+
rmSync4(path, { force: true });
|
|
4275
4464
|
} catch (error) {
|
|
4276
4465
|
log?.(`pid-file ${path} cleanup failed: ${String(error)}`);
|
|
4277
4466
|
}
|
|
@@ -4321,12 +4510,12 @@ function launchDetachedSupervisor(options) {
|
|
|
4321
4510
|
}
|
|
4322
4511
|
function controlSocketPath(pidFile) {
|
|
4323
4512
|
const hash = createHash2("sha256").update(resolve(pidFile)).digest("hex").slice(0, 16);
|
|
4324
|
-
return
|
|
4513
|
+
return join7(tmpdir(), `sechroom-fleet-${hash}.sock`);
|
|
4325
4514
|
}
|
|
4326
4515
|
function startControlServer(socketPath, handlers, options = {}) {
|
|
4327
4516
|
const log = options.log ?? (() => {
|
|
4328
4517
|
});
|
|
4329
|
-
|
|
4518
|
+
rmSync4(socketPath, { force: true });
|
|
4330
4519
|
const dispatch = (req) => {
|
|
4331
4520
|
switch (req.command) {
|
|
4332
4521
|
case "status":
|
|
@@ -4388,7 +4577,7 @@ function startControlServer(socketPath, handlers, options = {}) {
|
|
|
4388
4577
|
socketPath,
|
|
4389
4578
|
close: () => new Promise((res) => {
|
|
4390
4579
|
server.close(() => {
|
|
4391
|
-
|
|
4580
|
+
rmSync4(socketPath, { force: true });
|
|
4392
4581
|
res();
|
|
4393
4582
|
});
|
|
4394
4583
|
})
|
|
@@ -4467,29 +4656,88 @@ function sendControlCommand(socketPath, request, options = {}) {
|
|
|
4467
4656
|
});
|
|
4468
4657
|
}
|
|
4469
4658
|
|
|
4659
|
+
// src/executor-run/review-target.ts
|
|
4660
|
+
async function verifyReviewTarget(git, task) {
|
|
4661
|
+
const target = task.reviewExecution;
|
|
4662
|
+
if (!target) return { ok: true };
|
|
4663
|
+
const [remote, head] = await Promise.all([
|
|
4664
|
+
git("git", ["remote", "get-url", "origin"]),
|
|
4665
|
+
git("git", ["rev-parse", "HEAD"])
|
|
4666
|
+
]);
|
|
4667
|
+
const observedRepository = remote.ok ? canonicalRepository(remote.stdout) : void 0;
|
|
4668
|
+
const observedHead = head.ok ? head.stdout.trim().toLowerCase() || void 0 : void 0;
|
|
4669
|
+
const failures = [];
|
|
4670
|
+
if (observedRepository !== target.repository)
|
|
4671
|
+
failures.push(
|
|
4672
|
+
`repository required ${target.repository}, observed ${observedRepository ?? "(unavailable)"}`
|
|
4673
|
+
);
|
|
4674
|
+
if (observedHead !== target.headCommit)
|
|
4675
|
+
failures.push(
|
|
4676
|
+
`head required ${target.headCommit}, observed ${observedHead ?? "(unavailable)"}`
|
|
4677
|
+
);
|
|
4678
|
+
const evidence = [
|
|
4679
|
+
`required repository: ${target.repository}`,
|
|
4680
|
+
`observed repository: ${observedRepository ?? "(unavailable)"}`,
|
|
4681
|
+
`required base: ${target.baseCommit}`,
|
|
4682
|
+
`required head: ${target.headCommit}`,
|
|
4683
|
+
`observed head: ${observedHead ?? "(unavailable)"}`
|
|
4684
|
+
].join("\n");
|
|
4685
|
+
return {
|
|
4686
|
+
ok: failures.length === 0,
|
|
4687
|
+
reason: failures.length > 0 ? `review target mismatch before turn: ${failures.join("; ")}` : void 0,
|
|
4688
|
+
evidence
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
function canonicalRepository(remote) {
|
|
4692
|
+
let value = remote.trim().split(/\r?\n/, 1)[0] ?? "";
|
|
4693
|
+
if (!value) return void 0;
|
|
4694
|
+
if (value.startsWith("git@")) {
|
|
4695
|
+
const separator = value.indexOf(":");
|
|
4696
|
+
if (separator < 0) return void 0;
|
|
4697
|
+
value = value.slice(separator + 1);
|
|
4698
|
+
} else {
|
|
4699
|
+
try {
|
|
4700
|
+
const parsed = new URL(value);
|
|
4701
|
+
if (parsed.protocol === "file:") return void 0;
|
|
4702
|
+
value = parsed.pathname;
|
|
4703
|
+
} catch {
|
|
4704
|
+
return void 0;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
value = value.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
4708
|
+
const parts = value.split("/").filter(Boolean);
|
|
4709
|
+
if (parts.length < 2) return void 0;
|
|
4710
|
+
const owner = parts.at(-2);
|
|
4711
|
+
const repository = parts.at(-1);
|
|
4712
|
+
if (!owner || !repository) return void 0;
|
|
4713
|
+
const part = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4714
|
+
if (!part.test(owner) || !part.test(repository)) return void 0;
|
|
4715
|
+
return `${owner}/${repository}`.toLowerCase();
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4470
4718
|
// src/commands/telemetry.ts
|
|
4471
4719
|
import {
|
|
4472
4720
|
existsSync as existsSync7,
|
|
4473
4721
|
mkdirSync as mkdirSync8,
|
|
4474
|
-
readFileSync as
|
|
4475
|
-
rmSync as
|
|
4722
|
+
readFileSync as readFileSync7,
|
|
4723
|
+
rmSync as rmSync5,
|
|
4476
4724
|
writeFileSync as writeFileSync7
|
|
4477
4725
|
} from "fs";
|
|
4478
4726
|
import { homedir as homedir4 } from "os";
|
|
4479
|
-
import { dirname as dirname7, join as
|
|
4727
|
+
import { dirname as dirname7, join as join10, parse, resolve as resolve2, sep } from "path";
|
|
4480
4728
|
|
|
4481
4729
|
// src/commands/hook-install.ts
|
|
4482
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as
|
|
4483
|
-
import { delimiter, dirname as dirname6, join as
|
|
4730
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4731
|
+
import { delimiter, dirname as dirname6, join as join9 } from "path";
|
|
4484
4732
|
|
|
4485
4733
|
// src/setup/clients.ts
|
|
4486
4734
|
import { existsSync as existsSync5 } from "fs";
|
|
4487
4735
|
import { homedir as homedir3 } from "os";
|
|
4488
|
-
import { dirname as dirname5, join as
|
|
4736
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
4489
4737
|
function claudeDesktopConfigPath(home) {
|
|
4490
4738
|
switch (process.platform) {
|
|
4491
4739
|
case "darwin":
|
|
4492
|
-
return
|
|
4740
|
+
return join8(
|
|
4493
4741
|
home,
|
|
4494
4742
|
"Library",
|
|
4495
4743
|
"Application Support",
|
|
@@ -4497,19 +4745,20 @@ function claudeDesktopConfigPath(home) {
|
|
|
4497
4745
|
"claude_desktop_config.json"
|
|
4498
4746
|
);
|
|
4499
4747
|
case "win32":
|
|
4500
|
-
return
|
|
4501
|
-
process.env.APPDATA ??
|
|
4748
|
+
return join8(
|
|
4749
|
+
process.env.APPDATA ?? join8(home, "AppData", "Roaming"),
|
|
4502
4750
|
"Claude",
|
|
4503
4751
|
"claude_desktop_config.json"
|
|
4504
4752
|
);
|
|
4505
4753
|
default:
|
|
4506
|
-
return
|
|
4754
|
+
return join8(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4507
4755
|
}
|
|
4508
4756
|
}
|
|
4509
4757
|
function clientTargets(cwd, opts = {}) {
|
|
4510
4758
|
const home = homedir3();
|
|
4511
|
-
const claudeDir = opts.claudeDir ??
|
|
4512
|
-
const codexHome = opts.codexHome === void 0 ?
|
|
4759
|
+
const claudeDir = opts.claudeDir ?? join8(home, ".claude");
|
|
4760
|
+
const codexHome = opts.codexHome === void 0 ? join8(home, ".codex") : opts.codexHome;
|
|
4761
|
+
const codexConfigPath = opts.codexScope === "project" ? join8(cwd, ".codex", "config.toml") : codexHome ? join8(codexHome, "config.toml") : null;
|
|
4513
4762
|
return {
|
|
4514
4763
|
"claude-code": {
|
|
4515
4764
|
key: "claude-code",
|
|
@@ -4517,10 +4766,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4517
4766
|
mcp: {
|
|
4518
4767
|
surfaceKey: "claude-code",
|
|
4519
4768
|
sectionType: SectionType.McpConfig,
|
|
4520
|
-
path:
|
|
4769
|
+
path: join8(cwd, ".mcp.json"),
|
|
4521
4770
|
format: "json"
|
|
4522
4771
|
},
|
|
4523
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
4772
|
+
instruction: { surfaceKey: "claude-code", path: join8(cwd, "CLAUDE.md") }
|
|
4524
4773
|
},
|
|
4525
4774
|
"claude-desktop": {
|
|
4526
4775
|
key: "claude-desktop",
|
|
@@ -4533,19 +4782,19 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4533
4782
|
},
|
|
4534
4783
|
instruction: {
|
|
4535
4784
|
surfaceKey: "claude-desktop",
|
|
4536
|
-
path:
|
|
4785
|
+
path: join8(claudeDir, "CLAUDE.md")
|
|
4537
4786
|
}
|
|
4538
4787
|
},
|
|
4539
4788
|
codex: {
|
|
4540
4789
|
key: "codex",
|
|
4541
4790
|
label: "Codex CLI",
|
|
4542
|
-
mcp:
|
|
4791
|
+
mcp: codexConfigPath ? {
|
|
4543
4792
|
surfaceKey: "chatgpt",
|
|
4544
4793
|
sectionType: SectionType.McpConfigToml,
|
|
4545
|
-
path:
|
|
4794
|
+
path: codexConfigPath,
|
|
4546
4795
|
format: "toml"
|
|
4547
4796
|
} : null,
|
|
4548
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
4797
|
+
instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
|
|
4549
4798
|
},
|
|
4550
4799
|
cursor: {
|
|
4551
4800
|
key: "cursor",
|
|
@@ -4553,10 +4802,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4553
4802
|
mcp: {
|
|
4554
4803
|
surfaceKey: "claude-code",
|
|
4555
4804
|
sectionType: SectionType.McpConfig,
|
|
4556
|
-
path:
|
|
4805
|
+
path: join8(cwd, ".cursor", "mcp.json"),
|
|
4557
4806
|
format: "json"
|
|
4558
4807
|
},
|
|
4559
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
4808
|
+
instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
|
|
4560
4809
|
},
|
|
4561
4810
|
antigravity: {
|
|
4562
4811
|
key: "antigravity",
|
|
@@ -4570,10 +4819,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4570
4819
|
mcp: {
|
|
4571
4820
|
surfaceKey: "antigravity",
|
|
4572
4821
|
sectionType: SectionType.McpConfig,
|
|
4573
|
-
path:
|
|
4822
|
+
path: join8(home, ".gemini", "config", "mcp_config.json"),
|
|
4574
4823
|
format: "json"
|
|
4575
4824
|
},
|
|
4576
|
-
instruction: { surfaceKey: "antigravity", path:
|
|
4825
|
+
instruction: { surfaceKey: "antigravity", path: join8(cwd, "AGENTS.md") }
|
|
4577
4826
|
}
|
|
4578
4827
|
};
|
|
4579
4828
|
}
|
|
@@ -4593,9 +4842,9 @@ function detectInstalledClients(cwd) {
|
|
|
4593
4842
|
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4594
4843
|
detected.push("claude-desktop");
|
|
4595
4844
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4596
|
-
if (existsSync5(
|
|
4845
|
+
if (existsSync5(join8(home, ".cursor")) || existsSync5(join8(cwd, ".cursor")))
|
|
4597
4846
|
detected.push("cursor");
|
|
4598
|
-
if (existsSync5(
|
|
4847
|
+
if (existsSync5(join8(home, ".gemini"))) detected.push("antigravity");
|
|
4599
4848
|
return detected;
|
|
4600
4849
|
}
|
|
4601
4850
|
|
|
@@ -4639,12 +4888,12 @@ function mergeHooks(config2, commands) {
|
|
|
4639
4888
|
}
|
|
4640
4889
|
function readJsonConfig2(path) {
|
|
4641
4890
|
if (!existsSync6(path)) return {};
|
|
4642
|
-
const raw =
|
|
4891
|
+
const raw = readFileSync6(path, "utf8");
|
|
4643
4892
|
if (!raw.trim()) return {};
|
|
4644
4893
|
return JSON.parse(raw);
|
|
4645
4894
|
}
|
|
4646
4895
|
function installHooksJson(path, commands, dryRun) {
|
|
4647
|
-
const existed = existsSync6(path) &&
|
|
4896
|
+
const existed = existsSync6(path) && readFileSync6(path, "utf8").trim().length > 0;
|
|
4648
4897
|
const config2 = readJsonConfig2(path);
|
|
4649
4898
|
const added = mergeHooks(config2, commands);
|
|
4650
4899
|
if (added === 0 && existed) return { path, status: "current" };
|
|
@@ -4655,12 +4904,12 @@ function installHooksJson(path, commands, dryRun) {
|
|
|
4655
4904
|
return { path, status: existed ? "merged" : "created" };
|
|
4656
4905
|
}
|
|
4657
4906
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
4658
|
-
return installHooksJson(
|
|
4907
|
+
return installHooksJson(join9(claudeDir, "settings.json"), commands, dryRun);
|
|
4659
4908
|
}
|
|
4660
4909
|
function installCodexCommands(codexHome, commands, dryRun) {
|
|
4661
4910
|
return [
|
|
4662
|
-
installHooksJson(
|
|
4663
|
-
installCodexFeatureFlag(
|
|
4911
|
+
installHooksJson(join9(codexHome, "hooks.json"), commands, dryRun),
|
|
4912
|
+
installCodexFeatureFlag(join9(codexHome, "config.toml"), dryRun)
|
|
4664
4913
|
];
|
|
4665
4914
|
}
|
|
4666
4915
|
function ensureCodexFeaturesHooks(content) {
|
|
@@ -4685,7 +4934,7 @@ function ensureCodexFeaturesHooks(content) {
|
|
|
4685
4934
|
}
|
|
4686
4935
|
function installCodexFeatureFlag(path, dryRun) {
|
|
4687
4936
|
const existed = existsSync6(path);
|
|
4688
|
-
const content = existed ?
|
|
4937
|
+
const content = existed ? readFileSync6(path, "utf8") : "";
|
|
4689
4938
|
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
4690
4939
|
if (!changed) return { path, status: "current" };
|
|
4691
4940
|
if (!dryRun) {
|
|
@@ -4715,11 +4964,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
4715
4964
|
const out = [];
|
|
4716
4965
|
for (const surface of surfaces) {
|
|
4717
4966
|
if (surface === "claude") {
|
|
4718
|
-
const path =
|
|
4967
|
+
const path = join9(opts.claudeDir, "settings.json");
|
|
4719
4968
|
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
4720
4969
|
} else {
|
|
4721
|
-
const hooksJson = installHooksJson(
|
|
4722
|
-
const featureFlag = installCodexFeatureFlag(
|
|
4970
|
+
const hooksJson = installHooksJson(join9(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
4971
|
+
const featureFlag = installCodexFeatureFlag(join9(opts.codexHome, "config.toml"), opts.dryRun);
|
|
4723
4972
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
4724
4973
|
}
|
|
4725
4974
|
}
|
|
@@ -4739,7 +4988,7 @@ function isSechroomOnPath() {
|
|
|
4739
4988
|
for (const dir of pathEnv.split(delimiter)) {
|
|
4740
4989
|
if (!dir) continue;
|
|
4741
4990
|
for (const name of names) {
|
|
4742
|
-
if (existsSync6(
|
|
4991
|
+
if (existsSync6(join9(dir, name))) return true;
|
|
4743
4992
|
}
|
|
4744
4993
|
}
|
|
4745
4994
|
return false;
|
|
@@ -4784,6 +5033,13 @@ function registerTelemetry(program2) {
|
|
|
4784
5033
|
).option("--text <s>", "Raw/parsed payload text").option("--approval <state>", "Approval gate state (approval events)").option(
|
|
4785
5034
|
"--verdict <v>",
|
|
4786
5035
|
"Typed verdict (terminal events): pass | soft-fail | plan-invalid | blocked"
|
|
5036
|
+
).option(
|
|
5037
|
+
"--originating-review-id <id>",
|
|
5038
|
+
"Existing review entity id that caused this rework; never creates a review"
|
|
5039
|
+
).option(
|
|
5040
|
+
"--review-round <n>",
|
|
5041
|
+
"Review round for the originating review",
|
|
5042
|
+
parseIntOpt
|
|
4787
5043
|
).action(async (opts, cmd) => {
|
|
4788
5044
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4789
5045
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -4796,7 +5052,9 @@ function registerTelemetry(program2) {
|
|
|
4796
5052
|
contextWindow: opts.contextWindow ?? null,
|
|
4797
5053
|
text: opts.text ?? null,
|
|
4798
5054
|
approvalState: opts.approval ?? null,
|
|
4799
|
-
verdict: opts.verdict ?? null
|
|
5055
|
+
verdict: opts.verdict ?? null,
|
|
5056
|
+
originatingReviewId: opts.originatingReviewId,
|
|
5057
|
+
reviewRound: opts.reviewRound
|
|
4800
5058
|
};
|
|
4801
5059
|
let body;
|
|
4802
5060
|
try {
|
|
@@ -4837,14 +5095,23 @@ function registerTelemetry(program2) {
|
|
|
4837
5095
|
).requiredOption(
|
|
4838
5096
|
"--decomposition <id>",
|
|
4839
5097
|
"Decomposition id this session executes"
|
|
4840
|
-
).requiredOption("--task <id>", "Task id this session executes").
|
|
5098
|
+
).requiredOption("--task <id>", "Task id this session executes").option(
|
|
5099
|
+
"--originating-review-id <id>",
|
|
5100
|
+
"Existing review entity id driving this rework; never creates a review"
|
|
5101
|
+
).option(
|
|
5102
|
+
"--review-round <n>",
|
|
5103
|
+
"Review round for the originating review",
|
|
5104
|
+
parseIntOpt
|
|
5105
|
+
).action((opts, cmd) => {
|
|
4841
5106
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4842
|
-
const dir =
|
|
5107
|
+
const dir = join10(process.cwd(), ".sechroom");
|
|
4843
5108
|
mkdirSync8(dir, { recursive: true });
|
|
4844
|
-
const path =
|
|
5109
|
+
const path = join10(dir, BINDING_FILE);
|
|
4845
5110
|
const binding = {
|
|
4846
5111
|
decompositionId: opts.decomposition,
|
|
4847
|
-
taskId: opts.task
|
|
5112
|
+
taskId: opts.task,
|
|
5113
|
+
originatingReviewId: opts.originatingReviewId,
|
|
5114
|
+
reviewRound: opts.reviewRound
|
|
4848
5115
|
};
|
|
4849
5116
|
writeFileSync7(path, JSON.stringify(binding, null, 2) + "\n");
|
|
4850
5117
|
ensureStateDirIgnored(process.cwd());
|
|
@@ -4861,9 +5128,9 @@ function registerTelemetry(program2) {
|
|
|
4861
5128
|
});
|
|
4862
5129
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
4863
5130
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4864
|
-
const path =
|
|
5131
|
+
const path = join10(process.cwd(), ".sechroom", BINDING_FILE);
|
|
4865
5132
|
const existed = existsSync7(path);
|
|
4866
|
-
if (existed)
|
|
5133
|
+
if (existed) rmSync5(path);
|
|
4867
5134
|
if (json) emit({ unbound: existed, path }, true);
|
|
4868
5135
|
else
|
|
4869
5136
|
process.stdout.write(
|
|
@@ -4889,7 +5156,9 @@ function registerTelemetry(program2) {
|
|
|
4889
5156
|
input,
|
|
4890
5157
|
usage,
|
|
4891
5158
|
binding.taskId,
|
|
4892
|
-
configRoot
|
|
5159
|
+
configRoot,
|
|
5160
|
+
binding.originatingReviewId ?? null,
|
|
5161
|
+
binding.reviewRound ?? null
|
|
4893
5162
|
);
|
|
4894
5163
|
if (events.length === 0) return process.exit(0);
|
|
4895
5164
|
const taskId = await taskIdForHook(cfg, binding);
|
|
@@ -4983,11 +5252,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
4983
5252
|
function findBinding(start) {
|
|
4984
5253
|
let dir = start;
|
|
4985
5254
|
for (; ; ) {
|
|
4986
|
-
const path =
|
|
5255
|
+
const path = join10(dir, ".sechroom", BINDING_FILE);
|
|
4987
5256
|
if (existsSync7(path)) {
|
|
4988
5257
|
try {
|
|
4989
5258
|
const b = JSON.parse(
|
|
4990
|
-
|
|
5259
|
+
readFileSync7(path, "utf8")
|
|
4991
5260
|
);
|
|
4992
5261
|
if (b.decompositionId && b.taskId)
|
|
4993
5262
|
return {
|
|
@@ -4995,6 +5264,8 @@ function findBinding(start) {
|
|
|
4995
5264
|
taskId: b.taskId,
|
|
4996
5265
|
activeTaskCheckedAt: b.activeTaskCheckedAt,
|
|
4997
5266
|
lifecycleWarning: b.lifecycleWarning,
|
|
5267
|
+
originatingReviewId: b.originatingReviewId,
|
|
5268
|
+
reviewRound: b.reviewRound,
|
|
4998
5269
|
path
|
|
4999
5270
|
};
|
|
5000
5271
|
if (b.decompositionId && b.invalidatedTaskId && b.invalidatedReason === "terminal-task")
|
|
@@ -5002,6 +5273,8 @@ function findBinding(start) {
|
|
|
5002
5273
|
decompositionId: b.decompositionId,
|
|
5003
5274
|
invalidatedTaskId: b.invalidatedTaskId,
|
|
5004
5275
|
invalidatedReason: b.invalidatedReason,
|
|
5276
|
+
originatingReviewId: b.originatingReviewId,
|
|
5277
|
+
reviewRound: b.reviewRound,
|
|
5005
5278
|
path
|
|
5006
5279
|
};
|
|
5007
5280
|
} catch {
|
|
@@ -5064,7 +5337,7 @@ async function getTaskLifecycleVerdict(cfg, binding) {
|
|
|
5064
5337
|
function markLifecycleWarningIfCurrent(binding) {
|
|
5065
5338
|
try {
|
|
5066
5339
|
const current = JSON.parse(
|
|
5067
|
-
|
|
5340
|
+
readFileSync7(binding.path, "utf8")
|
|
5068
5341
|
);
|
|
5069
5342
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
5070
5343
|
return;
|
|
@@ -5072,7 +5345,9 @@ function markLifecycleWarningIfCurrent(binding) {
|
|
|
5072
5345
|
decompositionId: binding.decompositionId,
|
|
5073
5346
|
taskId: binding.taskId,
|
|
5074
5347
|
activeTaskCheckedAt: current.activeTaskCheckedAt,
|
|
5075
|
-
lifecycleWarning: "insufficient-permission"
|
|
5348
|
+
lifecycleWarning: "insufficient-permission",
|
|
5349
|
+
originatingReviewId: current.originatingReviewId,
|
|
5350
|
+
reviewRound: current.reviewRound
|
|
5076
5351
|
};
|
|
5077
5352
|
writeFileSync7(binding.path, JSON.stringify(warned, null, 2) + "\n");
|
|
5078
5353
|
} catch {
|
|
@@ -5081,14 +5356,16 @@ function markLifecycleWarningIfCurrent(binding) {
|
|
|
5081
5356
|
function cacheActiveBindingIfCurrent(binding) {
|
|
5082
5357
|
try {
|
|
5083
5358
|
const current = JSON.parse(
|
|
5084
|
-
|
|
5359
|
+
readFileSync7(binding.path, "utf8")
|
|
5085
5360
|
);
|
|
5086
5361
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
5087
5362
|
return;
|
|
5088
5363
|
const cached = {
|
|
5089
5364
|
decompositionId: binding.decompositionId,
|
|
5090
5365
|
taskId: binding.taskId,
|
|
5091
|
-
activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5366
|
+
activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5367
|
+
originatingReviewId: current.originatingReviewId,
|
|
5368
|
+
reviewRound: current.reviewRound
|
|
5092
5369
|
};
|
|
5093
5370
|
writeFileSync7(binding.path, JSON.stringify(cached, null, 2) + "\n");
|
|
5094
5371
|
} catch {
|
|
@@ -5097,7 +5374,7 @@ function cacheActiveBindingIfCurrent(binding) {
|
|
|
5097
5374
|
function invalidateBindingIfCurrent(binding) {
|
|
5098
5375
|
try {
|
|
5099
5376
|
const current = JSON.parse(
|
|
5100
|
-
|
|
5377
|
+
readFileSync7(binding.path, "utf8")
|
|
5101
5378
|
);
|
|
5102
5379
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
5103
5380
|
return;
|
|
@@ -5107,7 +5384,9 @@ function invalidateBindingIfCurrent(binding) {
|
|
|
5107
5384
|
{
|
|
5108
5385
|
decompositionId: binding.decompositionId,
|
|
5109
5386
|
invalidatedTaskId: binding.taskId,
|
|
5110
|
-
invalidatedReason: "terminal-task"
|
|
5387
|
+
invalidatedReason: "terminal-task",
|
|
5388
|
+
originatingReviewId: current.originatingReviewId,
|
|
5389
|
+
reviewRound: current.reviewRound
|
|
5111
5390
|
},
|
|
5112
5391
|
null,
|
|
5113
5392
|
2
|
|
@@ -5122,7 +5401,7 @@ function parseTranscript(path) {
|
|
|
5122
5401
|
let tokensOut = 0;
|
|
5123
5402
|
let contextUsed = 0;
|
|
5124
5403
|
let model = "";
|
|
5125
|
-
for (const line of
|
|
5404
|
+
for (const line of readFileSync7(path, "utf8").split("\n")) {
|
|
5126
5405
|
if (!line.trim()) continue;
|
|
5127
5406
|
let obj;
|
|
5128
5407
|
try {
|
|
@@ -5139,14 +5418,20 @@ function parseTranscript(path) {
|
|
|
5139
5418
|
if (obj.message?.model) model = obj.message.model;
|
|
5140
5419
|
}
|
|
5141
5420
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5142
|
-
return {
|
|
5421
|
+
return {
|
|
5422
|
+
tokensIn,
|
|
5423
|
+
tokensOut,
|
|
5424
|
+
contextUsed,
|
|
5425
|
+
contextWindow: windowFor(model, contextUsed),
|
|
5426
|
+
modelId: model || null
|
|
5427
|
+
};
|
|
5143
5428
|
}
|
|
5144
5429
|
function windowFor(model, contextUsed = 0) {
|
|
5145
5430
|
const m = model.toLowerCase();
|
|
5146
5431
|
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5147
5432
|
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5148
5433
|
}
|
|
5149
|
-
function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
5434
|
+
function buildHookEvents(input, usage, taskId, configRoot = null, originatingReviewId = null, reviewRound = null) {
|
|
5150
5435
|
const events = [];
|
|
5151
5436
|
const base = (kind, over) => ({
|
|
5152
5437
|
taskId,
|
|
@@ -5160,6 +5445,8 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
|
5160
5445
|
verdict: null,
|
|
5161
5446
|
modelId: null,
|
|
5162
5447
|
configRoot,
|
|
5448
|
+
originatingReviewId,
|
|
5449
|
+
reviewRound,
|
|
5163
5450
|
...over
|
|
5164
5451
|
});
|
|
5165
5452
|
if (usage) {
|
|
@@ -5188,7 +5475,9 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
|
5188
5475
|
break;
|
|
5189
5476
|
case "Stop":
|
|
5190
5477
|
case "SubagentStop":
|
|
5191
|
-
events.push(
|
|
5478
|
+
events.push(
|
|
5479
|
+
base("Terminal", { text: input.last_assistant_message ?? null })
|
|
5480
|
+
);
|
|
5192
5481
|
break;
|
|
5193
5482
|
}
|
|
5194
5483
|
return events;
|
|
@@ -5209,7 +5498,7 @@ function resolveClaudeConfigRoot(configuredRoot, transcriptPath) {
|
|
|
5209
5498
|
function normalizeClaudeConfigRoot(candidate) {
|
|
5210
5499
|
const trimmed = candidate?.trim();
|
|
5211
5500
|
if (!trimmed) return null;
|
|
5212
|
-
const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith(`~${sep}`) ?
|
|
5501
|
+
const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith(`~${sep}`) ? join10(homedir4(), trimmed.slice(2)) : trimmed;
|
|
5213
5502
|
let normalized = resolve2(expanded);
|
|
5214
5503
|
const rootLength = parse(normalized).root.length;
|
|
5215
5504
|
while (normalized.length > rootLength && normalized.endsWith(sep))
|
|
@@ -5283,10 +5572,10 @@ function registerExecutorRunCommand(executor) {
|
|
|
5283
5572
|
fail("--detach and --foreground are mutually exclusive");
|
|
5284
5573
|
const detach = !foreground;
|
|
5285
5574
|
const pidFile = resolve3(
|
|
5286
|
-
opts.pidFile ? String(opts.pidFile) :
|
|
5575
|
+
opts.pidFile ? String(opts.pidFile) : join11(process.cwd(), ".sechroom", "fleet.pid")
|
|
5287
5576
|
);
|
|
5288
5577
|
const logFile = resolve3(
|
|
5289
|
-
opts.logFile ? String(opts.logFile) :
|
|
5578
|
+
opts.logFile ? String(opts.logFile) : join11(process.cwd(), ".sechroom", "fleet.log")
|
|
5290
5579
|
);
|
|
5291
5580
|
if (detach && !isDetachedChild) {
|
|
5292
5581
|
await readFleetConfig(String(opts.config));
|
|
@@ -5572,7 +5861,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5572
5861
|
if (excludeTags.length)
|
|
5573
5862
|
log(`excluding offers tagged: ${excludeTags.join(", ")}`);
|
|
5574
5863
|
const rootDir = resolve3(String(opts.root));
|
|
5575
|
-
const usageLogPath =
|
|
5864
|
+
const usageLogPath = join11(
|
|
5576
5865
|
rootDir,
|
|
5577
5866
|
".sechroom",
|
|
5578
5867
|
`executor-usage-${located.state.instanceKey.replace(/[^\w.-]/g, "-")}.jsonl`
|
|
@@ -5702,6 +5991,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5702
5991
|
},
|
|
5703
5992
|
claimNext: async () => await fleetInbox.waitForClaim() ?? await claimNext(request, instance.id, log, excludeTags, skipLog),
|
|
5704
5993
|
loadTask: (memoryId) => materializeClaimedTask(request, memoryId),
|
|
5994
|
+
verifyTaskBinding: (task) => verifyReviewTarget(gitRunner, task),
|
|
5705
5995
|
startLeaseHeartbeat: (claim) => startLeaseHeartbeat(
|
|
5706
5996
|
() => request(
|
|
5707
5997
|
`/me/executor-task-leases/${encodeURIComponent(claim.leaseId)}/heartbeat`,
|
|
@@ -5830,7 +6120,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5830
6120
|
}
|
|
5831
6121
|
function resolveFleetPidFile(flag) {
|
|
5832
6122
|
return resolve3(
|
|
5833
|
-
flag ? String(flag) :
|
|
6123
|
+
flag ? String(flag) : join11(process.cwd(), ".sechroom", "fleet.pid")
|
|
5834
6124
|
);
|
|
5835
6125
|
}
|
|
5836
6126
|
function requireLiveSupervisor(pidFile) {
|
|
@@ -5966,6 +6256,13 @@ var CODEX_EXECUTOR_HOOKS = {
|
|
|
5966
6256
|
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
5967
6257
|
Stop: EXECUTOR_PULSE_COMMAND
|
|
5968
6258
|
};
|
|
6259
|
+
function claimNextHint(claim) {
|
|
6260
|
+
if (claim.outcome === "NoOffer")
|
|
6261
|
+
return "no offer is currently standing for this instance";
|
|
6262
|
+
if (claim.lease?.id && claim.claimToken)
|
|
6263
|
+
return `claimed lease ${claim.lease.id} \u2014 keep it alive with the claimToken above, then close it out with work_executor_complete`;
|
|
6264
|
+
return void 0;
|
|
6265
|
+
}
|
|
5969
6266
|
function registerExecutor(program2) {
|
|
5970
6267
|
const executor = program2.command("executor").description(
|
|
5971
6268
|
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
@@ -6078,8 +6375,8 @@ function registerExecutor(program2) {
|
|
|
6078
6375
|
fail("refresh-after must be shorter than the TTL");
|
|
6079
6376
|
const sem = readSem();
|
|
6080
6377
|
const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
|
|
6081
|
-
const statePath =
|
|
6082
|
-
const previous = existsSync8(statePath) ? JSON.parse(
|
|
6378
|
+
const statePath = join12(checkout, ".sechroom", EXECUTOR_STATE);
|
|
6379
|
+
const previous = existsSync8(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : void 0;
|
|
6083
6380
|
const canUpdateExisting = previous?.instanceId && previous.instanceKey === instanceKey && previous.laneId === laneId && previous.runtime === (runtime.toLowerCase() === "codex" ? "codex" : "claude-code") && previous.relayId === opts.relay && previous.connectorId === connector;
|
|
6084
6381
|
const state = {
|
|
6085
6382
|
schemaVersion: 1,
|
|
@@ -6108,8 +6405,8 @@ function registerExecutor(program2) {
|
|
|
6108
6405
|
}
|
|
6109
6406
|
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
|
|
6110
6407
|
(target) => target.dir
|
|
6111
|
-
) : [
|
|
6112
|
-
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [
|
|
6408
|
+
) : [join12(checkout, ".claude")];
|
|
6409
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join12(checkout, ".codex")];
|
|
6113
6410
|
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
6114
6411
|
for (const target of hookTargets) {
|
|
6115
6412
|
const results = surface === "claude" ? [
|
|
@@ -6324,6 +6621,29 @@ function registerExecutor(program2) {
|
|
|
6324
6621
|
);
|
|
6325
6622
|
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6326
6623
|
});
|
|
6624
|
+
executor.command("claim <id>").description(
|
|
6625
|
+
"Claim the next dispatch offer standing for this exact instance (one-shot attended self-claim)"
|
|
6626
|
+
).option(
|
|
6627
|
+
"--idempotency-key <key>",
|
|
6628
|
+
"Replay the same claim on retry instead of taking another offer (default: a fresh key per call)"
|
|
6629
|
+
).action(async (id, opts, cmd) => {
|
|
6630
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
6631
|
+
const data = await api(
|
|
6632
|
+
cfg,
|
|
6633
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers/claim-next`,
|
|
6634
|
+
{
|
|
6635
|
+
method: "POST",
|
|
6636
|
+
body: JSON.stringify({
|
|
6637
|
+
idempotencyKey: opts.idempotencyKey ?? `cli:${randomUUID().replace(/-/g, "")}`
|
|
6638
|
+
})
|
|
6639
|
+
}
|
|
6640
|
+
);
|
|
6641
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6642
|
+
if (cmd.optsWithGlobals().json) return;
|
|
6643
|
+
const hint = claimNextHint(data);
|
|
6644
|
+
if (hint) process.stderr.write(style.dim(`${hint}
|
|
6645
|
+
`));
|
|
6646
|
+
});
|
|
6327
6647
|
executor.command("proxy-claim <generationId>").description(
|
|
6328
6648
|
"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)"
|
|
6329
6649
|
).requiredOption(
|
|
@@ -6644,13 +6964,13 @@ function refreshRuntimeVersion(state, binaryOverride) {
|
|
|
6644
6964
|
function readExecutorState(start = process.cwd()) {
|
|
6645
6965
|
const semPath = resolveSemPathForRead(start);
|
|
6646
6966
|
const sem = semPath ? readSem(semPath) : void 0;
|
|
6647
|
-
const path =
|
|
6648
|
-
sem ? dirname8(sem.path) :
|
|
6967
|
+
const path = join12(
|
|
6968
|
+
sem ? dirname8(sem.path) : join12(start, ".sechroom"),
|
|
6649
6969
|
EXECUTOR_STATE
|
|
6650
6970
|
);
|
|
6651
6971
|
if (!existsSync8(path)) return void 0;
|
|
6652
6972
|
return {
|
|
6653
|
-
state: JSON.parse(
|
|
6973
|
+
state: JSON.parse(readFileSync8(path, "utf8")),
|
|
6654
6974
|
path
|
|
6655
6975
|
};
|
|
6656
6976
|
}
|
|
@@ -6820,7 +7140,7 @@ function registerChannel(program2) {
|
|
|
6820
7140
|
channel.command("install").description(
|
|
6821
7141
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
6822
7142
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
6823
|
-
const path =
|
|
7143
|
+
const path = join13(process.cwd(), ".mcp.json");
|
|
6824
7144
|
const dryRun = Boolean(opts.dryRun);
|
|
6825
7145
|
const name = "sechroom-channel";
|
|
6826
7146
|
const args = ["channel", "mcp"];
|
|
@@ -6917,36 +7237,69 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
6917
7237
|
}, intervalMilliseconds);
|
|
6918
7238
|
return () => cancel(timer);
|
|
6919
7239
|
}
|
|
7240
|
+
var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
|
|
7241
|
+
function classifyLeaseHeartbeatFailure(error) {
|
|
7242
|
+
if (!(error instanceof HttpError)) return "retry";
|
|
7243
|
+
if (error.status === 408 || error.status === 429) return "retry";
|
|
7244
|
+
if (error.status < 400 || error.status >= 500) return "retry";
|
|
7245
|
+
if (error.status === 409 && /\bLease is Released\b/.test(error.body))
|
|
7246
|
+
return "released";
|
|
7247
|
+
return "terminal";
|
|
7248
|
+
}
|
|
7249
|
+
function leaseHeartbeatStoppedLine(leaseId, error) {
|
|
7250
|
+
if (!(error instanceof HttpError))
|
|
7251
|
+
return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
|
|
7252
|
+
return `lease heartbeat stopped for ${leaseId} (${error.status}): ${problemDetail(error.body)}`;
|
|
7253
|
+
}
|
|
7254
|
+
function problemDetail(body) {
|
|
7255
|
+
try {
|
|
7256
|
+
const parsed = JSON.parse(body);
|
|
7257
|
+
const detail = parsed.detail ?? parsed.title;
|
|
7258
|
+
if (typeof detail === "string" && detail.length > 0) return detail;
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
return body;
|
|
7262
|
+
}
|
|
6920
7263
|
function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4, dependencies = {}) {
|
|
6921
7264
|
const leaseId = claim.lease?.id;
|
|
6922
7265
|
const claimToken = claim.claimToken;
|
|
6923
7266
|
if (!leaseId || !claimToken) return void 0;
|
|
6924
|
-
const request = dependencies.request ??
|
|
7267
|
+
const request = dependencies.request ?? leaseHeartbeatApi;
|
|
6925
7268
|
const onError = dependencies.onError ?? ((value) => process.stderr.write(
|
|
6926
7269
|
err(`channel lease heartbeat failed: ${String(value)}
|
|
6927
7270
|
`)
|
|
6928
7271
|
));
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
{
|
|
6944
|
-
|
|
6945
|
-
|
|
7272
|
+
let stop;
|
|
7273
|
+
const beat = async () => {
|
|
7274
|
+
try {
|
|
7275
|
+
return await request(
|
|
7276
|
+
cfg,
|
|
7277
|
+
`/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
|
|
7278
|
+
{
|
|
7279
|
+
method: "POST",
|
|
7280
|
+
body: JSON.stringify({
|
|
7281
|
+
claimToken,
|
|
7282
|
+
tokenVersion: claim.tokenVersion ?? 1
|
|
7283
|
+
})
|
|
7284
|
+
}
|
|
7285
|
+
);
|
|
7286
|
+
} catch (error) {
|
|
7287
|
+
const disposition = classifyLeaseHeartbeatFailure(error);
|
|
7288
|
+
if (disposition === "retry") throw error;
|
|
7289
|
+
stop?.();
|
|
7290
|
+
dependencies.onLeaseTerminal?.(leaseId, error);
|
|
7291
|
+
if (disposition === "terminal")
|
|
7292
|
+
onError(leaseHeartbeatStoppedLine(leaseId, error));
|
|
7293
|
+
return void 0;
|
|
6946
7294
|
}
|
|
6947
|
-
|
|
7295
|
+
};
|
|
7296
|
+
stop = startLeaseHeartbeat(beat, onError, intervalMilliseconds, {
|
|
7297
|
+
setInterval: dependencies.setInterval,
|
|
7298
|
+
clearInterval: dependencies.clearInterval
|
|
7299
|
+
});
|
|
7300
|
+
return stop;
|
|
6948
7301
|
}
|
|
6949
|
-
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
7302
|
+
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
|
|
6950
7303
|
const stops = /* @__PURE__ */ new Map();
|
|
6951
7304
|
const intervalMilliseconds = Math.max(
|
|
6952
7305
|
1e3,
|
|
@@ -6959,10 +7312,20 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
|
6959
7312
|
const stop = startChannelTaskLeaseHeartbeat(
|
|
6960
7313
|
cfg,
|
|
6961
7314
|
claim,
|
|
6962
|
-
intervalMilliseconds
|
|
7315
|
+
intervalMilliseconds,
|
|
7316
|
+
{
|
|
7317
|
+
...dependencies,
|
|
7318
|
+
// The beat has already cancelled its own timer; drop the dead entry so a
|
|
7319
|
+
// later re-claim of the same lease id can start a fresh beat, and so the
|
|
7320
|
+
// map does not accumulate stopped leases for the life of the channel.
|
|
7321
|
+
onLeaseTerminal: (id) => {
|
|
7322
|
+
stops.delete(id);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
6963
7325
|
);
|
|
6964
7326
|
if (stop) stops.set(leaseId, stop);
|
|
6965
7327
|
},
|
|
7328
|
+
activeLeaseIds: () => [...stops.keys()],
|
|
6966
7329
|
stop: () => {
|
|
6967
7330
|
for (const stop of stops.values()) stop();
|
|
6968
7331
|
stops.clear();
|
|
@@ -7024,7 +7387,7 @@ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {})
|
|
|
7024
7387
|
}
|
|
7025
7388
|
function readMcpConfig(path) {
|
|
7026
7389
|
if (!existsSync9(path)) return {};
|
|
7027
|
-
const raw =
|
|
7390
|
+
const raw = readFileSync9(path, "utf8");
|
|
7028
7391
|
if (!raw.trim()) return {};
|
|
7029
7392
|
try {
|
|
7030
7393
|
return JSON.parse(raw);
|
|
@@ -7198,12 +7561,12 @@ Examples:
|
|
|
7198
7561
|
|
|
7199
7562
|
// src/commands/checkpoint.ts
|
|
7200
7563
|
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
7201
|
-
import { dirname as dirname14, join as
|
|
7564
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
7202
7565
|
|
|
7203
7566
|
// src/commands/hook.ts
|
|
7204
7567
|
import { createHash as createHash4 } from "crypto";
|
|
7205
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as
|
|
7206
|
-
import { dirname as dirname13, join as
|
|
7568
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
7569
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
7207
7570
|
|
|
7208
7571
|
// src/commands/lane-commit-hook.ts
|
|
7209
7572
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -7211,7 +7574,7 @@ import {
|
|
|
7211
7574
|
chmodSync,
|
|
7212
7575
|
existsSync as existsSync10,
|
|
7213
7576
|
mkdirSync as mkdirSync11,
|
|
7214
|
-
readFileSync as
|
|
7577
|
+
readFileSync as readFileSync10,
|
|
7215
7578
|
renameSync,
|
|
7216
7579
|
unlinkSync,
|
|
7217
7580
|
writeFileSync as writeFileSync10
|
|
@@ -7232,7 +7595,7 @@ function resolveCheckoutLane(start) {
|
|
|
7232
7595
|
return pin ? applyWorktreeLaneSuffix(pin, start) : void 0;
|
|
7233
7596
|
}
|
|
7234
7597
|
function appendLaneTrailer(messagePath, lane) {
|
|
7235
|
-
const original =
|
|
7598
|
+
const original = readFileSync10(messagePath, "utf8").replace(/\r\n/g, "\n");
|
|
7236
7599
|
const lines = original.split("\n");
|
|
7237
7600
|
const scissorsIndex = lines.findIndex(
|
|
7238
7601
|
(line) => /^#\s*-+\s*>8\s*-+\s*$/.test(line)
|
|
@@ -7304,7 +7667,7 @@ function resolveHookPath(root, hookName) {
|
|
|
7304
7667
|
function removeLegacyPrepareCommitMsgLeg(root) {
|
|
7305
7668
|
const path = resolveHookPath(root, LEGACY_HOOK_NAME);
|
|
7306
7669
|
if (!existsSync10(path)) return;
|
|
7307
|
-
const current =
|
|
7670
|
+
const current = readFileSync10(path, "utf8");
|
|
7308
7671
|
const pattern = managedBlockPattern();
|
|
7309
7672
|
if (!pattern.test(current)) return;
|
|
7310
7673
|
const next = current.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
@@ -7324,7 +7687,7 @@ function installLaneCommitHook(start) {
|
|
|
7324
7687
|
}).trim();
|
|
7325
7688
|
removeLegacyPrepareCommitMsgLeg(root);
|
|
7326
7689
|
const path = resolveHookPath(root, HOOK_NAME);
|
|
7327
|
-
const current = existsSync10(path) ?
|
|
7690
|
+
const current = existsSync10(path) ? readFileSync10(path, "utf8") : "";
|
|
7328
7691
|
let next;
|
|
7329
7692
|
if (current && !isShellHook(current)) {
|
|
7330
7693
|
const incumbentPath = nextIncumbentPath(path);
|
|
@@ -7347,25 +7710,25 @@ function installLaneCommitHook(start) {
|
|
|
7347
7710
|
}
|
|
7348
7711
|
|
|
7349
7712
|
// src/commands/session-context.ts
|
|
7350
|
-
import { randomUUID as
|
|
7351
|
-
import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as
|
|
7352
|
-
import { dirname as dirname12, join as
|
|
7713
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
7714
|
+
import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
7715
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
7353
7716
|
|
|
7354
7717
|
// src/setup/skill-composition-materialise.ts
|
|
7355
|
-
import { createHash as createHash3, randomUUID } from "crypto";
|
|
7718
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
7356
7719
|
import {
|
|
7357
7720
|
existsSync as existsSync11,
|
|
7358
7721
|
mkdirSync as mkdirSync12,
|
|
7359
|
-
readdirSync as
|
|
7360
|
-
readFileSync as
|
|
7722
|
+
readdirSync as readdirSync3,
|
|
7723
|
+
readFileSync as readFileSync11,
|
|
7361
7724
|
renameSync as renameSync2,
|
|
7362
7725
|
rmdirSync,
|
|
7363
|
-
rmSync as
|
|
7364
|
-
statSync as
|
|
7726
|
+
rmSync as rmSync6,
|
|
7727
|
+
statSync as statSync3,
|
|
7365
7728
|
writeFileSync as writeFileSync11
|
|
7366
7729
|
} from "fs";
|
|
7367
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, join as
|
|
7368
|
-
var COMPILED_SKILL_LOCK =
|
|
7730
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, join as join14, relative, resolve as resolve5 } from "path";
|
|
7731
|
+
var COMPILED_SKILL_LOCK = join14(
|
|
7369
7732
|
".sechroom",
|
|
7370
7733
|
"compiled-skill-materialisation.json"
|
|
7371
7734
|
);
|
|
@@ -7430,7 +7793,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7430
7793
|
desired.delete(key);
|
|
7431
7794
|
}
|
|
7432
7795
|
}
|
|
7433
|
-
const lockPath =
|
|
7796
|
+
const lockPath = join14(cwd, COMPILED_SKILL_LOCK);
|
|
7434
7797
|
const previous = readLock(lockPath, destinations);
|
|
7435
7798
|
const next = {
|
|
7436
7799
|
version: 2,
|
|
@@ -7441,7 +7804,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7441
7804
|
const path = skillPath(value.skillsRoot, value.skill.name);
|
|
7442
7805
|
const directory = dirname11(path);
|
|
7443
7806
|
const prior = previous.entries[key];
|
|
7444
|
-
const existing = existsSync11(path) ?
|
|
7807
|
+
const existing = existsSync11(path) ? readFileSync11(path, "utf8") : void 0;
|
|
7445
7808
|
const existingHash = existing === void 0 ? void 0 : sha256(existing);
|
|
7446
7809
|
const owned = prior !== void 0 && prior.skillsRoot === value.skillsRoot && existing !== void 0 && (existingHash === prior.contentHash || existingHash === prior.previousContentHash);
|
|
7447
7810
|
const recoverableMissing = prior !== void 0 && existing === void 0;
|
|
@@ -7489,7 +7852,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7489
7852
|
};
|
|
7490
7853
|
writeLock(lockPath, next);
|
|
7491
7854
|
mkdirSync12(directory, { recursive: true });
|
|
7492
|
-
if (existingHash !== void 0 && (!existsSync11(path) || sha256(
|
|
7855
|
+
if (existingHash !== void 0 && (!existsSync11(path) || sha256(readFileSync11(path, "utf8")) !== existingHash)) {
|
|
7493
7856
|
delete next.entries[key];
|
|
7494
7857
|
writeLock(lockPath, next);
|
|
7495
7858
|
items.push({
|
|
@@ -7572,7 +7935,7 @@ function entryKey(target, name, skillsRoot) {
|
|
|
7572
7935
|
return `${target}:${sha256(skillsRoot)}:${name}`;
|
|
7573
7936
|
}
|
|
7574
7937
|
function skillPath(skillsRoot, name) {
|
|
7575
|
-
return
|
|
7938
|
+
return join14(skillsRoot, name, "SKILL.md");
|
|
7576
7939
|
}
|
|
7577
7940
|
function withFinalNewline(body) {
|
|
7578
7941
|
return body.endsWith("\n") ? body : body + "\n";
|
|
@@ -7587,7 +7950,7 @@ function readLock(path, destinations) {
|
|
|
7587
7950
|
)
|
|
7588
7951
|
);
|
|
7589
7952
|
try {
|
|
7590
|
-
const parsed = JSON.parse(
|
|
7953
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7591
7954
|
if (parsed.version === 2 && parsed.entries && typeof parsed.entries === "object") {
|
|
7592
7955
|
const entries = {};
|
|
7593
7956
|
for (const [key, candidate] of Object.entries(parsed.entries)) {
|
|
@@ -7618,22 +7981,22 @@ function writeAtomic(path, body) {
|
|
|
7618
7981
|
renameSync2(temporary, path);
|
|
7619
7982
|
}
|
|
7620
7983
|
function removeOwnedFile(path, acceptableHashes) {
|
|
7621
|
-
const quarantine = `${path}.sechroom-retire-${process.pid}-${
|
|
7984
|
+
const quarantine = `${path}.sechroom-retire-${process.pid}-${randomUUID2()}`;
|
|
7622
7985
|
try {
|
|
7623
7986
|
renameSync2(path, quarantine);
|
|
7624
7987
|
} catch (error) {
|
|
7625
7988
|
if (error.code === "ENOENT") return "missing";
|
|
7626
7989
|
throw error;
|
|
7627
7990
|
}
|
|
7628
|
-
const quarantinedHash = sha256(
|
|
7991
|
+
const quarantinedHash = sha256(readFileSync11(quarantine, "utf8"));
|
|
7629
7992
|
if (acceptableHashes.includes(quarantinedHash)) {
|
|
7630
|
-
|
|
7993
|
+
rmSync6(quarantine);
|
|
7631
7994
|
return "removed";
|
|
7632
7995
|
}
|
|
7633
7996
|
if (!existsSync11(path)) {
|
|
7634
7997
|
renameSync2(quarantine, path);
|
|
7635
7998
|
} else {
|
|
7636
|
-
renameSync2(quarantine, `${path}.sechroom-preserved-${
|
|
7999
|
+
renameSync2(quarantine, `${path}.sechroom-preserved-${randomUUID2()}`);
|
|
7637
8000
|
}
|
|
7638
8001
|
return "collision";
|
|
7639
8002
|
}
|
|
@@ -7642,7 +8005,7 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7642
8005
|
if (!path) return;
|
|
7643
8006
|
let current = "";
|
|
7644
8007
|
try {
|
|
7645
|
-
current =
|
|
8008
|
+
current = readFileSync11(path, "utf8");
|
|
7646
8009
|
} catch {
|
|
7647
8010
|
}
|
|
7648
8011
|
const withoutOwnedBlock = removeOwnedExcludeBlock(current);
|
|
@@ -7657,14 +8020,14 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7657
8020
|
writeAtomic(path, updated);
|
|
7658
8021
|
}
|
|
7659
8022
|
function gitExcludePath(cwd) {
|
|
7660
|
-
const dotGit =
|
|
8023
|
+
const dotGit = join14(cwd, ".git");
|
|
7661
8024
|
try {
|
|
7662
|
-
if (
|
|
7663
|
-
const pointer =
|
|
8025
|
+
if (statSync3(dotGit).isDirectory()) return join14(dotGit, "info", "exclude");
|
|
8026
|
+
const pointer = readFileSync11(dotGit, "utf8").trim();
|
|
7664
8027
|
if (!pointer.startsWith("gitdir:")) return void 0;
|
|
7665
8028
|
const raw = pointer.slice("gitdir:".length).trim();
|
|
7666
8029
|
const gitDir = isAbsolute2(raw) ? raw : resolve5(cwd, raw);
|
|
7667
|
-
return
|
|
8030
|
+
return join14(gitDir, "info", "exclude");
|
|
7668
8031
|
} catch {
|
|
7669
8032
|
return void 0;
|
|
7670
8033
|
}
|
|
@@ -7681,13 +8044,13 @@ function removeOwnedExcludeBlock(body) {
|
|
|
7681
8044
|
}
|
|
7682
8045
|
function removeDirectoryIfEmpty(path) {
|
|
7683
8046
|
try {
|
|
7684
|
-
if (
|
|
8047
|
+
if (readdirSync3(path).length === 0) rmdirSync(path);
|
|
7685
8048
|
} catch {
|
|
7686
8049
|
}
|
|
7687
8050
|
}
|
|
7688
8051
|
|
|
7689
8052
|
// src/commands/session-context.ts
|
|
7690
|
-
var DYNAMIC_AGENT_CONTEXT_FILE =
|
|
8053
|
+
var DYNAMIC_AGENT_CONTEXT_FILE = join15(".sechroom", "CLAUDE.md");
|
|
7691
8054
|
function checkoutRoot(start) {
|
|
7692
8055
|
const semPath = resolveSemPathForRead(start);
|
|
7693
8056
|
return semPath ? dirname12(dirname12(semPath)) : start;
|
|
@@ -7724,7 +8087,7 @@ function renderSessionContext(result, lane) {
|
|
|
7724
8087
|
}
|
|
7725
8088
|
function writeSessionContext(start, lane, result, options = {}) {
|
|
7726
8089
|
const root = checkoutRoot(start);
|
|
7727
|
-
const path =
|
|
8090
|
+
const path = join15(root, DYNAMIC_AGENT_CONTEXT_FILE);
|
|
7728
8091
|
const skills = materialiseCompiledSkillCompositions(
|
|
7729
8092
|
root,
|
|
7730
8093
|
result.status === "hold" ? {
|
|
@@ -7740,13 +8103,13 @@ function writeSessionContext(start, lane, result, options = {}) {
|
|
|
7740
8103
|
);
|
|
7741
8104
|
const context = renderSessionContext(result, lane);
|
|
7742
8105
|
mkdirSync13(dirname12(path), { recursive: true });
|
|
7743
|
-
const temporaryPath = `${path}.${process.pid}.${
|
|
8106
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
7744
8107
|
try {
|
|
7745
8108
|
writeFileSync12(temporaryPath, context.endsWith("\n") ? context : `${context}
|
|
7746
8109
|
`, "utf8");
|
|
7747
8110
|
renameSync3(temporaryPath, path);
|
|
7748
8111
|
} catch (error) {
|
|
7749
|
-
|
|
8112
|
+
rmSync7(temporaryPath, { force: true });
|
|
7750
8113
|
throw error;
|
|
7751
8114
|
}
|
|
7752
8115
|
return { status: result.status, path, context, skills };
|
|
@@ -7797,12 +8160,12 @@ function resolveLane(flagLane, cwd) {
|
|
|
7797
8160
|
if (!base) return void 0;
|
|
7798
8161
|
return applyWorktreeLaneSuffix(base, start);
|
|
7799
8162
|
}
|
|
7800
|
-
var INTENT_FILE =
|
|
8163
|
+
var INTENT_FILE = join16(".sechroom", "continuity.json");
|
|
7801
8164
|
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
7802
8165
|
function resolveIntentPath(start) {
|
|
7803
8166
|
let dir = start;
|
|
7804
8167
|
for (; ; ) {
|
|
7805
|
-
const candidate =
|
|
8168
|
+
const candidate = join16(dir, INTENT_FILE);
|
|
7806
8169
|
if (existsSync12(candidate)) return candidate;
|
|
7807
8170
|
const parent = dirname13(dir);
|
|
7808
8171
|
if (parent === dir) return void 0;
|
|
@@ -7813,7 +8176,7 @@ function readIntent(start) {
|
|
|
7813
8176
|
const path = resolveIntentPath(start);
|
|
7814
8177
|
if (!path) return void 0;
|
|
7815
8178
|
try {
|
|
7816
|
-
return JSON.parse(
|
|
8179
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
7817
8180
|
} catch {
|
|
7818
8181
|
return void 0;
|
|
7819
8182
|
}
|
|
@@ -7867,14 +8230,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7867
8230
|
}
|
|
7868
8231
|
function ledgerPath(start) {
|
|
7869
8232
|
const intent = resolveIntentPath(start);
|
|
7870
|
-
const dir = intent ? dirname13(intent) :
|
|
7871
|
-
return
|
|
8233
|
+
const dir = intent ? dirname13(intent) : join16(start, ".sechroom");
|
|
8234
|
+
return join16(dir, ".checkpoint-state.json");
|
|
7872
8235
|
}
|
|
7873
8236
|
function readLedger(start) {
|
|
7874
8237
|
try {
|
|
7875
8238
|
const p = ledgerPath(start);
|
|
7876
8239
|
if (!existsSync12(p)) return {};
|
|
7877
|
-
return JSON.parse(
|
|
8240
|
+
return JSON.parse(readFileSync12(p, "utf8"));
|
|
7878
8241
|
} catch {
|
|
7879
8242
|
return {};
|
|
7880
8243
|
}
|
|
@@ -7905,7 +8268,7 @@ function unchangedSinceLastPush(start, intent) {
|
|
|
7905
8268
|
const path = resolveIntentPath(start);
|
|
7906
8269
|
if (path && ledger.lastMtimeMs != null) {
|
|
7907
8270
|
try {
|
|
7908
|
-
if (
|
|
8271
|
+
if (statSync4(path).mtimeMs <= ledger.lastMtimeMs) return true;
|
|
7909
8272
|
} catch {
|
|
7910
8273
|
}
|
|
7911
8274
|
}
|
|
@@ -7917,7 +8280,7 @@ function recordPush(start, intent) {
|
|
|
7917
8280
|
const path = resolveIntentPath(start);
|
|
7918
8281
|
let mtimeMs;
|
|
7919
8282
|
try {
|
|
7920
|
-
if (path) mtimeMs =
|
|
8283
|
+
if (path) mtimeMs = statSync4(path).mtimeMs;
|
|
7921
8284
|
} catch {
|
|
7922
8285
|
mtimeMs = void 0;
|
|
7923
8286
|
}
|
|
@@ -8241,7 +8604,7 @@ Examples:
|
|
|
8241
8604
|
const client = await makeClient(cfg);
|
|
8242
8605
|
return client.POST("/continuity/snapshots", { body });
|
|
8243
8606
|
});
|
|
8244
|
-
const path = resolveIntentPath(cwd) ??
|
|
8607
|
+
const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
|
|
8245
8608
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
8246
8609
|
mkdirSync15(dirname14(path), { recursive: true });
|
|
8247
8610
|
writeFileSync14(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
@@ -8258,7 +8621,7 @@ Examples:
|
|
|
8258
8621
|
}
|
|
8259
8622
|
|
|
8260
8623
|
// src/commands/close.ts
|
|
8261
|
-
import { readFileSync as
|
|
8624
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
8262
8625
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
8263
8626
|
function registerClose(program2) {
|
|
8264
8627
|
program2.command("close").description(
|
|
@@ -8299,7 +8662,7 @@ Examples:
|
|
|
8299
8662
|
);
|
|
8300
8663
|
let bodyText;
|
|
8301
8664
|
try {
|
|
8302
|
-
bodyText = opts.file ?
|
|
8665
|
+
bodyText = opts.file ? readFileSync13(opts.file, "utf8") : readFileSync13(0, "utf8");
|
|
8303
8666
|
} catch {
|
|
8304
8667
|
fail(
|
|
8305
8668
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -8770,7 +9133,7 @@ Examples:
|
|
|
8770
9133
|
);
|
|
8771
9134
|
});
|
|
8772
9135
|
workPlan.command("append <decompositionId>").description(
|
|
8773
|
-
"Append hand-authored task(s) to a running work plan; they land proposed until ratified (POST /decompositions/{id}/append-tasks)"
|
|
9136
|
+
"Append hand-authored task(s) to a running work plan; they land proposed until ratified. Verification tasks may carry reviewExecution { kind: local-code-review, round, repository, baseCommit, headCommit, preferredInstanceKey?, preferredLaneId? } (POST /decompositions/{id}/append-tasks)"
|
|
8774
9137
|
).requiredOption(
|
|
8775
9138
|
"--file <path>",
|
|
8776
9139
|
"JSON file containing { tasks, gates? } (the AppendTasksInput shape); use - for stdin"
|
|
@@ -9190,13 +9553,13 @@ function registerGitHub(program2) {
|
|
|
9190
9553
|
}
|
|
9191
9554
|
|
|
9192
9555
|
// src/commands/herdr.ts
|
|
9193
|
-
import { readFileSync as
|
|
9556
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
9194
9557
|
import { basename as basename3 } from "path";
|
|
9195
9558
|
|
|
9196
9559
|
// src/herdr/client.ts
|
|
9197
9560
|
import { createConnection as createConnection2 } from "net";
|
|
9198
9561
|
import { homedir as homedir5 } from "os";
|
|
9199
|
-
import { join as
|
|
9562
|
+
import { join as join18 } from "path";
|
|
9200
9563
|
var DEFAULT_HERDR_SOCKET_RELATIVE = ".config/herdr/herdr.sock";
|
|
9201
9564
|
var HerdrUnreachableError = class extends Error {
|
|
9202
9565
|
constructor(socketPath, reason) {
|
|
@@ -9221,7 +9584,7 @@ function resolveSocketPath(flag, env = process.env, home = homedir5()) {
|
|
|
9221
9584
|
if (fromFlag) return fromFlag;
|
|
9222
9585
|
const fromEnv = env.HERDR_SOCKET?.trim();
|
|
9223
9586
|
if (fromEnv) return fromEnv;
|
|
9224
|
-
return
|
|
9587
|
+
return join18(home, DEFAULT_HERDR_SOCKET_RELATIVE);
|
|
9225
9588
|
}
|
|
9226
9589
|
function expandTarget(target) {
|
|
9227
9590
|
const trimmed = target.trim();
|
|
@@ -9561,7 +9924,7 @@ function parseSource(value, fallback = DEFAULT_READ_SOURCE) {
|
|
|
9561
9924
|
}
|
|
9562
9925
|
return match;
|
|
9563
9926
|
}
|
|
9564
|
-
function resolveSendText(textArgs, useStdin, readStdin5 = () =>
|
|
9927
|
+
function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync14(0, "utf8")) {
|
|
9565
9928
|
if (useStdin) {
|
|
9566
9929
|
if (textArgs.length > 0) {
|
|
9567
9930
|
throw new Error(
|
|
@@ -10301,11 +10664,11 @@ Examples:
|
|
|
10301
10664
|
}
|
|
10302
10665
|
|
|
10303
10666
|
// src/commands/memory.ts
|
|
10304
|
-
import { readFileSync as
|
|
10667
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
10305
10668
|
|
|
10306
10669
|
// src/commands/memory-import.ts
|
|
10307
|
-
import { readdirSync as
|
|
10308
|
-
import { basename as basename4, join as
|
|
10670
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync, statSync as statSync5 } from "fs";
|
|
10671
|
+
import { basename as basename4, join as join19, resolve as resolve6 } from "path";
|
|
10309
10672
|
var MARKDOWN_RE = /\.(md|markdown)$/i;
|
|
10310
10673
|
function isMarkdownPath(path) {
|
|
10311
10674
|
return MARKDOWN_RE.test(path);
|
|
@@ -10332,18 +10695,18 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10332
10695
|
files.push(path);
|
|
10333
10696
|
};
|
|
10334
10697
|
const walk = (dir) => {
|
|
10335
|
-
const entries =
|
|
10698
|
+
const entries = readdirSync4(dir, { withFileTypes: true }).sort(
|
|
10336
10699
|
(a, b) => a.name.localeCompare(b.name)
|
|
10337
10700
|
);
|
|
10338
10701
|
for (const entry of entries) {
|
|
10339
10702
|
if (entry.name.startsWith(".")) continue;
|
|
10340
|
-
const child =
|
|
10703
|
+
const child = join19(dir, entry.name);
|
|
10341
10704
|
let isDirectory = entry.isDirectory();
|
|
10342
10705
|
let isFile = entry.isFile();
|
|
10343
10706
|
if (entry.isSymbolicLink()) {
|
|
10344
10707
|
let target;
|
|
10345
10708
|
try {
|
|
10346
|
-
target =
|
|
10709
|
+
target = statSync5(child);
|
|
10347
10710
|
} catch {
|
|
10348
10711
|
skipped.push({ path: child, reason: "broken symlink" });
|
|
10349
10712
|
continue;
|
|
@@ -10381,7 +10744,7 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10381
10744
|
for (const input of inputs) {
|
|
10382
10745
|
let isDirectory;
|
|
10383
10746
|
try {
|
|
10384
|
-
isDirectory =
|
|
10747
|
+
isDirectory = statSync5(input).isDirectory();
|
|
10385
10748
|
} catch {
|
|
10386
10749
|
missing.push(input);
|
|
10387
10750
|
continue;
|
|
@@ -10397,7 +10760,7 @@ function buildImportPlan(collected) {
|
|
|
10397
10760
|
for (const path of collected.files) {
|
|
10398
10761
|
let text2;
|
|
10399
10762
|
try {
|
|
10400
|
-
text2 =
|
|
10763
|
+
text2 = readFileSync15(path, "utf8");
|
|
10401
10764
|
} catch (error) {
|
|
10402
10765
|
throw new Error(
|
|
10403
10766
|
`couldn't read ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -10498,7 +10861,7 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10498
10861
|
}
|
|
10499
10862
|
if (textOpt != null) return { text: textOpt, defaultTitle: null };
|
|
10500
10863
|
const fromStdin = fileOpt === "-";
|
|
10501
|
-
const text2 = fromStdin ?
|
|
10864
|
+
const text2 = fromStdin ? readFileSync16(0, "utf8") : readFileSync16(String(fileOpt), "utf8");
|
|
10502
10865
|
if (text2.trim().length === 0) {
|
|
10503
10866
|
fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
|
|
10504
10867
|
}
|
|
@@ -11150,16 +11513,16 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11150
11513
|
}
|
|
11151
11514
|
|
|
11152
11515
|
// src/setup/apply.ts
|
|
11153
|
-
import { createHash as createHash5, randomUUID as
|
|
11516
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
11154
11517
|
import {
|
|
11155
11518
|
chmodSync as chmodSync2,
|
|
11156
11519
|
copyFileSync,
|
|
11157
11520
|
existsSync as existsSync13,
|
|
11158
11521
|
mkdirSync as mkdirSync16,
|
|
11159
|
-
readFileSync as
|
|
11522
|
+
readFileSync as readFileSync17,
|
|
11160
11523
|
renameSync as renameSync4,
|
|
11161
|
-
rmSync as
|
|
11162
|
-
statSync as
|
|
11524
|
+
rmSync as rmSync8,
|
|
11525
|
+
statSync as statSync6,
|
|
11163
11526
|
writeFileSync as writeFileSync15
|
|
11164
11527
|
} from "fs";
|
|
11165
11528
|
import { dirname as dirname15 } from "path";
|
|
@@ -11226,7 +11589,7 @@ function ensureDir2(path) {
|
|
|
11226
11589
|
}
|
|
11227
11590
|
function readOr(path, fallback) {
|
|
11228
11591
|
try {
|
|
11229
|
-
return
|
|
11592
|
+
return readFileSync17(path, "utf8");
|
|
11230
11593
|
} catch {
|
|
11231
11594
|
return fallback;
|
|
11232
11595
|
}
|
|
@@ -11237,7 +11600,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
11237
11600
|
let current = {};
|
|
11238
11601
|
if (existed) {
|
|
11239
11602
|
try {
|
|
11240
|
-
current = JSON.parse(
|
|
11603
|
+
current = JSON.parse(readFileSync17(path, "utf8"));
|
|
11241
11604
|
} catch {
|
|
11242
11605
|
return {
|
|
11243
11606
|
kind: "mcp",
|
|
@@ -11607,24 +11970,24 @@ var defaultTomlFileOperations = {
|
|
|
11607
11970
|
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11608
11971
|
ensureDir2(path);
|
|
11609
11972
|
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11610
|
-
const temporary = `${path}.sechroom-${process.pid}-${
|
|
11973
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
11611
11974
|
const backup = `${path}.bak`;
|
|
11612
|
-
const backupTemporary = `${backup}.sechroom-${process.pid}-${
|
|
11613
|
-
const mode = existsSync13(path) ?
|
|
11975
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
11976
|
+
const mode = existsSync13(path) ? statSync6(path).mode & 4095 : 384;
|
|
11614
11977
|
try {
|
|
11615
11978
|
writeFileSync15(temporary, content, { mode });
|
|
11616
11979
|
chmodSync2(temporary, mode);
|
|
11617
|
-
validateToml(
|
|
11980
|
+
validateToml(readFileSync17(temporary, "utf8"));
|
|
11618
11981
|
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11619
11982
|
operations.copyFileSync(path, backupTemporary);
|
|
11620
11983
|
chmodSync2(backupTemporary, mode);
|
|
11621
|
-
validateToml(
|
|
11984
|
+
validateToml(readFileSync17(backupTemporary, "utf8"));
|
|
11622
11985
|
operations.renameSync(backupTemporary, backup);
|
|
11623
11986
|
}
|
|
11624
11987
|
operations.renameSync(temporary, path);
|
|
11625
11988
|
} finally {
|
|
11626
|
-
|
|
11627
|
-
|
|
11989
|
+
rmSync8(temporary, { force: true });
|
|
11990
|
+
rmSync8(backupTemporary, { force: true });
|
|
11628
11991
|
}
|
|
11629
11992
|
}
|
|
11630
11993
|
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
@@ -11913,7 +12276,7 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
11913
12276
|
|
|
11914
12277
|
// src/setup/skills-offer.ts
|
|
11915
12278
|
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync16 } from "fs";
|
|
11916
|
-
import { join as
|
|
12279
|
+
import { join as join20 } from "path";
|
|
11917
12280
|
|
|
11918
12281
|
// src/setup/lane-pin.ts
|
|
11919
12282
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -12029,8 +12392,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
12029
12392
|
if (skills.length > 0) {
|
|
12030
12393
|
const written = [];
|
|
12031
12394
|
for (const s of skills) {
|
|
12032
|
-
mkdirSync17(
|
|
12033
|
-
writeFileSync16(
|
|
12395
|
+
mkdirSync17(join20(sDir, s.name), { recursive: true });
|
|
12396
|
+
writeFileSync16(join20(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
12034
12397
|
written.push(s.name);
|
|
12035
12398
|
}
|
|
12036
12399
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -12042,7 +12405,7 @@ Found ${summary} available to you for ${surface}.
|
|
|
12042
12405
|
const written = [];
|
|
12043
12406
|
for (const a of agents) {
|
|
12044
12407
|
const file = `${a.name}.md`;
|
|
12045
|
-
writeFileSync16(
|
|
12408
|
+
writeFileSync16(join20(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
12046
12409
|
written.push(file);
|
|
12047
12410
|
}
|
|
12048
12411
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -12345,7 +12708,8 @@ Examples:
|
|
|
12345
12708
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12346
12709
|
const targets = clientTargets(process.cwd(), {
|
|
12347
12710
|
claudeDir: claudeTargets[0]?.dir,
|
|
12348
|
-
codexHome: codexHomes[0] ?? null
|
|
12711
|
+
codexHome: codexHomes[0] ?? null,
|
|
12712
|
+
codexScope: scope
|
|
12349
12713
|
});
|
|
12350
12714
|
const keys = resolveClientKeys(opts.client);
|
|
12351
12715
|
const json = g.json;
|
|
@@ -12637,15 +13001,30 @@ Examples:
|
|
|
12637
13001
|
"--client <list>",
|
|
12638
13002
|
`comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
12639
13003
|
DEFAULT_CLIENT_KEY
|
|
13004
|
+
).option(
|
|
13005
|
+
"--scope <scope>",
|
|
13006
|
+
"Codex MCP config scope: 'project' (<cwd>/.codex) or 'global' (CODEX_HOME / ~/.codex) \u2014 default project",
|
|
13007
|
+
"project"
|
|
12640
13008
|
).option("--dry-run", "print what would be written without writing", false).action(async (slug2, opts, cmd) => {
|
|
12641
13009
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12642
13010
|
const setup = await withSpinner(
|
|
12643
13011
|
"Fetching setup descriptors",
|
|
12644
13012
|
() => fetchSetup(cfg, slug2)
|
|
12645
13013
|
);
|
|
12646
|
-
|
|
13014
|
+
let scope;
|
|
13015
|
+
try {
|
|
13016
|
+
scope = resolveScope(opts.scope);
|
|
13017
|
+
} catch (error) {
|
|
13018
|
+
return fail(error.message);
|
|
13019
|
+
}
|
|
13020
|
+
const globals = cmd.optsWithGlobals();
|
|
13021
|
+
const codexHome = scope === "global" ? resolveCodexHomes({ override: globals.codexHome, scope })[0] ?? null : null;
|
|
13022
|
+
const targets = clientTargets(process.cwd(), {
|
|
13023
|
+
codexHome,
|
|
13024
|
+
codexScope: scope
|
|
13025
|
+
});
|
|
12647
13026
|
const keys = resolveClientKeys(opts.client);
|
|
12648
|
-
const json =
|
|
13027
|
+
const json = globals.json;
|
|
12649
13028
|
const result = [];
|
|
12650
13029
|
for (const key of keys) {
|
|
12651
13030
|
const target = targets[key];
|
|
@@ -12672,12 +13051,12 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
|
|
|
12672
13051
|
|
|
12673
13052
|
// src/commands/onboard.ts
|
|
12674
13053
|
import { existsSync as existsSync15 } from "fs";
|
|
12675
|
-
import { basename as basename5, join as
|
|
13054
|
+
import { basename as basename5, join as join22 } from "path";
|
|
12676
13055
|
|
|
12677
13056
|
// src/commands/fanout.ts
|
|
12678
13057
|
import { spawnSync } from "child_process";
|
|
12679
|
-
import { existsSync as existsSync14, readFileSync as
|
|
12680
|
-
import { isAbsolute as isAbsolute3, join as
|
|
13058
|
+
import { existsSync as existsSync14, readFileSync as readFileSync18, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
13059
|
+
import { isAbsolute as isAbsolute3, join as join21, resolve as resolve7 } from "path";
|
|
12681
13060
|
var ICON = {
|
|
12682
13061
|
refresh: "\u21BB",
|
|
12683
13062
|
bind: "+",
|
|
@@ -12690,20 +13069,20 @@ function resolveChildDir(path, root) {
|
|
|
12690
13069
|
function discoverChildren(root) {
|
|
12691
13070
|
let names;
|
|
12692
13071
|
try {
|
|
12693
|
-
names =
|
|
13072
|
+
names = readdirSync5(root);
|
|
12694
13073
|
} catch {
|
|
12695
13074
|
return [];
|
|
12696
13075
|
}
|
|
12697
13076
|
const out = [];
|
|
12698
13077
|
for (const name of names.sort()) {
|
|
12699
13078
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
12700
|
-
const dir =
|
|
13079
|
+
const dir = join21(root, name);
|
|
12701
13080
|
try {
|
|
12702
|
-
if (!
|
|
13081
|
+
if (!statSync7(dir).isDirectory()) continue;
|
|
12703
13082
|
} catch {
|
|
12704
13083
|
continue;
|
|
12705
13084
|
}
|
|
12706
|
-
if (existsSync14(
|
|
13085
|
+
if (existsSync14(join21(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
12707
13086
|
}
|
|
12708
13087
|
return out;
|
|
12709
13088
|
}
|
|
@@ -12711,7 +13090,7 @@ function readManifest(path) {
|
|
|
12711
13090
|
if (!existsSync14(path)) return null;
|
|
12712
13091
|
let parsed;
|
|
12713
13092
|
try {
|
|
12714
|
-
parsed = JSON.parse(
|
|
13093
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
12715
13094
|
} catch (err2) {
|
|
12716
13095
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
12717
13096
|
}
|
|
@@ -13179,7 +13558,7 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
13179
13558
|
reason: "directory does not exist"
|
|
13180
13559
|
};
|
|
13181
13560
|
}
|
|
13182
|
-
if (existsSync15(
|
|
13561
|
+
if (existsSync15(join22(dir, ".sechroom.json"))) {
|
|
13183
13562
|
return {
|
|
13184
13563
|
label: entry.path,
|
|
13185
13564
|
dir,
|
|
@@ -13286,7 +13665,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
13286
13665
|
async function runRecurse(cfg, g, opts) {
|
|
13287
13666
|
const { yes, dryRun, json } = opts;
|
|
13288
13667
|
const root = process.cwd();
|
|
13289
|
-
const manifestPath =
|
|
13668
|
+
const manifestPath = join22(root, ".sechroom", "repos.json");
|
|
13290
13669
|
const fromManifest = readManifest(manifestPath);
|
|
13291
13670
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
13292
13671
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -13535,20 +13914,15 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
13535
13914
|
yes,
|
|
13536
13915
|
process.cwd()
|
|
13537
13916
|
);
|
|
13538
|
-
const keys =
|
|
13539
|
-
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13540
|
-
process.stderr.write(
|
|
13541
|
-
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13542
|
-
`
|
|
13543
|
-
);
|
|
13544
|
-
}
|
|
13917
|
+
const keys = requestedKeys;
|
|
13545
13918
|
const setup = await withSpinner(
|
|
13546
13919
|
"Fetching setup descriptors",
|
|
13547
13920
|
() => fetchSetup(cfg)
|
|
13548
13921
|
);
|
|
13549
13922
|
const targets = clientTargets(process.cwd(), {
|
|
13550
13923
|
claudeDir: claudeTargets[0]?.dir,
|
|
13551
|
-
codexHome: codexHomes[0] ?? null
|
|
13924
|
+
codexHome: codexHomes[0] ?? null,
|
|
13925
|
+
codexScope: scope
|
|
13552
13926
|
});
|
|
13553
13927
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
13554
13928
|
if (!dryRun && !check) {
|
|
@@ -14078,31 +14452,31 @@ Examples:
|
|
|
14078
14452
|
|
|
14079
14453
|
// src/commands/reset.ts
|
|
14080
14454
|
import { homedir as homedir6 } from "os";
|
|
14081
|
-
import { join as
|
|
14082
|
-
import { existsSync as existsSync16, readFileSync as
|
|
14455
|
+
import { join as join23 } from "path";
|
|
14456
|
+
import { existsSync as existsSync16, readFileSync as readFileSync19, rmSync as rmSync9 } from "fs";
|
|
14083
14457
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
14084
|
-
var localSkillsDir = () =>
|
|
14085
|
-
var globalSkillsDir = () =>
|
|
14086
|
-
var localAgentsDir = () =>
|
|
14087
|
-
var globalAgentsDir = () =>
|
|
14458
|
+
var localSkillsDir = () => join23(process.cwd(), ".claude", "skills");
|
|
14459
|
+
var globalSkillsDir = () => join23(homedir6(), ".claude", "skills");
|
|
14460
|
+
var localAgentsDir = () => join23(process.cwd(), ".claude", "agents");
|
|
14461
|
+
var globalAgentsDir = () => join23(homedir6(), ".claude", "agents");
|
|
14088
14462
|
function removeMaterialisedSkills(dir) {
|
|
14089
14463
|
const removed = [];
|
|
14090
|
-
const lockPath =
|
|
14464
|
+
const lockPath = join23(dir, SKILLS_LOCK2);
|
|
14091
14465
|
if (!existsSync16(lockPath)) return removed;
|
|
14092
14466
|
try {
|
|
14093
|
-
const lock = JSON.parse(
|
|
14467
|
+
const lock = JSON.parse(readFileSync19(lockPath, "utf8"));
|
|
14094
14468
|
for (const entry of Object.values(lock)) {
|
|
14095
14469
|
for (const name of entry.skills ?? []) {
|
|
14096
|
-
const p =
|
|
14470
|
+
const p = join23(dir, name);
|
|
14097
14471
|
if (existsSync16(p)) {
|
|
14098
|
-
|
|
14472
|
+
rmSync9(p, { recursive: true, force: true });
|
|
14099
14473
|
removed.push(p);
|
|
14100
14474
|
}
|
|
14101
14475
|
}
|
|
14102
14476
|
}
|
|
14103
14477
|
} catch {
|
|
14104
14478
|
}
|
|
14105
|
-
|
|
14479
|
+
rmSync9(lockPath, { force: true });
|
|
14106
14480
|
removed.push(lockPath);
|
|
14107
14481
|
return removed;
|
|
14108
14482
|
}
|
|
@@ -14139,19 +14513,19 @@ function registerReset(program2) {
|
|
|
14139
14513
|
}
|
|
14140
14514
|
}
|
|
14141
14515
|
const removed = [];
|
|
14142
|
-
const stateDir =
|
|
14516
|
+
const stateDir = join23(process.cwd(), ".sechroom");
|
|
14143
14517
|
if (existsSync16(stateDir)) {
|
|
14144
|
-
|
|
14518
|
+
rmSync9(stateDir, { recursive: true, force: true });
|
|
14145
14519
|
removed.push(stateDir);
|
|
14146
14520
|
}
|
|
14147
|
-
const legacyCfg =
|
|
14521
|
+
const legacyCfg = join23(process.cwd(), ".sechroom.json");
|
|
14148
14522
|
if (existsSync16(legacyCfg)) {
|
|
14149
|
-
|
|
14523
|
+
rmSync9(legacyCfg, { force: true });
|
|
14150
14524
|
removed.push(legacyCfg);
|
|
14151
14525
|
}
|
|
14152
|
-
const legacySem =
|
|
14526
|
+
const legacySem = join23(process.cwd(), ".sem");
|
|
14153
14527
|
if (existsSync16(legacySem)) {
|
|
14154
|
-
|
|
14528
|
+
rmSync9(legacySem, { force: true });
|
|
14155
14529
|
removed.push(legacySem);
|
|
14156
14530
|
}
|
|
14157
14531
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -14176,8 +14550,8 @@ function registerReset(program2) {
|
|
|
14176
14550
|
}
|
|
14177
14551
|
|
|
14178
14552
|
// src/commands/skills.ts
|
|
14179
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
14180
|
-
import { join as
|
|
14553
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
|
|
14554
|
+
import { join as join24 } from "path";
|
|
14181
14555
|
function filenameFromDisposition(header) {
|
|
14182
14556
|
if (!header) return void 0;
|
|
14183
14557
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -14185,11 +14559,11 @@ function filenameFromDisposition(header) {
|
|
|
14185
14559
|
}
|
|
14186
14560
|
function resolveOutputPath(output, serverFilename) {
|
|
14187
14561
|
const filename = serverFilename || "skills.zip";
|
|
14188
|
-
if (!output) return
|
|
14189
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
14562
|
+
if (!output) return join24(process.cwd(), filename);
|
|
14563
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync8(output).isDirectory();
|
|
14190
14564
|
if (looksLikeDir) {
|
|
14191
14565
|
mkdirSync18(output, { recursive: true });
|
|
14192
|
-
return
|
|
14566
|
+
return join24(output, filename);
|
|
14193
14567
|
}
|
|
14194
14568
|
return output;
|
|
14195
14569
|
}
|
|
@@ -14235,6 +14609,7 @@ Examples:
|
|
|
14235
14609
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
14236
14610
|
$ sechroom skills list what's materialised on disk
|
|
14237
14611
|
$ sechroom skills clean remove the materialised skill files
|
|
14612
|
+
$ sechroom skills clean --prune-orphans also sweep skills orphaned by an earlier rename
|
|
14238
14613
|
$ sechroom skills preview --workspace wsp_abc render a draft bundle from source (no install)
|
|
14239
14614
|
$ sechroom skills package my-bundle download the installed bundle's skills as a zip
|
|
14240
14615
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
@@ -14248,7 +14623,7 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14248
14623
|
);
|
|
14249
14624
|
skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
14250
14625
|
skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
14251
|
-
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
14626
|
+
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--prune-orphans", "also remove sechroom-installed skill dirs that no lock entry claims (renamed/removed upstream)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
14252
14627
|
skills.command("preview").description("Render a workspace's draft bundle from source (no publish/install) and report the components").requiredOption("--workspace <id>", "workspace holding the draft bundle sources (wsp_\u2026)").option("--slug <slug>", "override the derived bundle slug").option("--title <title>", "override the derived bundle title").option("--version <version>", "override the derived bundle version").option("--default-install-parent <path>", "override the derived default install parent").option("--json", "machine output (the full RenderBundlePreviewResponse)").action(async (opts, cmd) => {
|
|
14253
14628
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
14254
14629
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -14395,8 +14770,8 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14395
14770
|
|
|
14396
14771
|
// src/commands/sweep.ts
|
|
14397
14772
|
import { existsSync as existsSync18 } from "fs";
|
|
14398
|
-
import { dirname as dirname16, join as
|
|
14399
|
-
var DEFAULT_MANIFEST =
|
|
14773
|
+
import { dirname as dirname16, join as join25, resolve as resolve8 } from "path";
|
|
14774
|
+
var DEFAULT_MANIFEST = join25(".sechroom", "repos.json");
|
|
14400
14775
|
function planEntry(entry, root) {
|
|
14401
14776
|
const dir = resolveChildDir(entry.path, root);
|
|
14402
14777
|
if (!existsSync18(dir)) {
|
|
@@ -14900,7 +15275,7 @@ async function readStdin4() {
|
|
|
14900
15275
|
function resolveVersion() {
|
|
14901
15276
|
try {
|
|
14902
15277
|
const pkg = JSON.parse(
|
|
14903
|
-
|
|
15278
|
+
readFileSync20(new URL("../package.json", import.meta.url), "utf8")
|
|
14904
15279
|
);
|
|
14905
15280
|
return pkg.version ?? "0.0.0";
|
|
14906
15281
|
} catch {
|