@sechroom/cli 2026.8.9-rc.6433a3deb → 2026.9.2
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 +960 -412
- 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";
|
|
@@ -1234,7 +1234,7 @@ ${mem.text.trim()}`
|
|
|
1234
1234
|
if (parts.length === 0) return null;
|
|
1235
1235
|
return { body: parts.join("\n\n"), refs };
|
|
1236
1236
|
}
|
|
1237
|
-
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1237
|
+
async function createOverride(cfg, template, personalWorkspaceId, source) {
|
|
1238
1238
|
const client = await makeClient(cfg);
|
|
1239
1239
|
const overrideTags = template.templateTags.filter(
|
|
1240
1240
|
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
@@ -1249,7 +1249,7 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1249
1249
|
type: "reference",
|
|
1250
1250
|
content: "{}",
|
|
1251
1251
|
confidence: 1,
|
|
1252
|
-
source
|
|
1252
|
+
source,
|
|
1253
1253
|
archetype: "Document",
|
|
1254
1254
|
title: template.title ?? null,
|
|
1255
1255
|
tags: overrideTags,
|
|
@@ -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
|
}
|
|
@@ -6711,9 +7031,11 @@ function registerChannel(program2) {
|
|
|
6711
7031
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
6712
7032
|
)
|
|
6713
7033
|
);
|
|
7034
|
+
const authExit = createAuthExpiredExit();
|
|
6714
7035
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
6715
7036
|
cfg,
|
|
6716
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7037
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7038
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
6717
7039
|
);
|
|
6718
7040
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
6719
7041
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -6752,13 +7074,11 @@ function registerChannel(program2) {
|
|
|
6752
7074
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
6753
7075
|
);
|
|
6754
7076
|
}
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
leaseHeartbeats.stop();
|
|
6761
|
-
}
|
|
7077
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7078
|
+
stopReconciliation,
|
|
7079
|
+
stopHeartbeat,
|
|
7080
|
+
leaseHeartbeats.stop
|
|
7081
|
+
]);
|
|
6762
7082
|
});
|
|
6763
7083
|
channel.command("mcp").description(
|
|
6764
7084
|
"Run as a Claude Code channel (local-stdio MCP server) \u2014 claim and push dispatched tasks into the session"
|
|
@@ -6781,9 +7101,11 @@ function registerChannel(program2) {
|
|
|
6781
7101
|
params: { content, meta }
|
|
6782
7102
|
});
|
|
6783
7103
|
});
|
|
7104
|
+
const authExit = createAuthExpiredExit();
|
|
6784
7105
|
const leaseHeartbeats = createChannelLeaseHeartbeatManager(
|
|
6785
7106
|
cfg,
|
|
6786
|
-
located.state.taskLeaseTtlSeconds ?? 120
|
|
7107
|
+
located.state.taskLeaseTtlSeconds ?? 120,
|
|
7108
|
+
{ onAuthExpired: authExit.onAuthExpired }
|
|
6787
7109
|
);
|
|
6788
7110
|
const drain = createClaimDrain(cfg, instance.id, deliver, {
|
|
6789
7111
|
onClaimed: leaseHeartbeats.onClaimed
|
|
@@ -6809,18 +7131,21 @@ function registerChannel(program2) {
|
|
|
6809
7131
|
`
|
|
6810
7132
|
)
|
|
6811
7133
|
);
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
7134
|
+
await holdChannelOpen(conn, authExit.released, [
|
|
7135
|
+
stopReconciliation,
|
|
7136
|
+
stopHeartbeat,
|
|
7137
|
+
leaseHeartbeats.stop,
|
|
7138
|
+
// The stdio transport keeps a stdin `data` listener, which holds the event
|
|
7139
|
+
// loop open for as long as the parent holds the pipe. Without this close, a
|
|
7140
|
+
// `process.exitCode` set on the auth path never becomes an exit and the
|
|
7141
|
+
// parent sees a connected-but-inert channel instead of a dead one.
|
|
7142
|
+
() => mcp.close()
|
|
7143
|
+
]);
|
|
6819
7144
|
});
|
|
6820
7145
|
channel.command("install").description(
|
|
6821
7146
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
6822
7147
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
6823
|
-
const path =
|
|
7148
|
+
const path = join13(process.cwd(), ".mcp.json");
|
|
6824
7149
|
const dryRun = Boolean(opts.dryRun);
|
|
6825
7150
|
const name = "sechroom-channel";
|
|
6826
7151
|
const args = ["channel", "mcp"];
|
|
@@ -6917,37 +7242,80 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
6917
7242
|
}, intervalMilliseconds);
|
|
6918
7243
|
return () => cancel(timer);
|
|
6919
7244
|
}
|
|
7245
|
+
var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
|
|
7246
|
+
function classifyLeaseHeartbeatFailure(error) {
|
|
7247
|
+
if (error instanceof AuthExpiredError) return "auth";
|
|
7248
|
+
if (!(error instanceof HttpError)) return "retry";
|
|
7249
|
+
if (error.status === 408 || error.status === 429) return "retry";
|
|
7250
|
+
if (error.status < 400 || error.status >= 500) return "retry";
|
|
7251
|
+
if (error.status === 409 && /\bLease is Released\b/.test(error.body))
|
|
7252
|
+
return "released";
|
|
7253
|
+
return "terminal";
|
|
7254
|
+
}
|
|
7255
|
+
function authExpiredLine(error) {
|
|
7256
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7257
|
+
return `auth expired \u2014 run \`sechroom login\` to re-authenticate: ${detail}`;
|
|
7258
|
+
}
|
|
7259
|
+
function leaseHeartbeatStoppedLine(leaseId, error) {
|
|
7260
|
+
if (!(error instanceof HttpError))
|
|
7261
|
+
return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
|
|
7262
|
+
return `lease heartbeat stopped for ${leaseId} (${error.status}): ${problemDetail(error.body)}`;
|
|
7263
|
+
}
|
|
7264
|
+
function problemDetail(body) {
|
|
7265
|
+
try {
|
|
7266
|
+
const parsed = JSON.parse(body);
|
|
7267
|
+
const detail = parsed.detail ?? parsed.title;
|
|
7268
|
+
if (typeof detail === "string" && detail.length > 0) return detail;
|
|
7269
|
+
} catch {
|
|
7270
|
+
}
|
|
7271
|
+
return body;
|
|
7272
|
+
}
|
|
6920
7273
|
function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4, dependencies = {}) {
|
|
6921
7274
|
const leaseId = claim.lease?.id;
|
|
6922
7275
|
const claimToken = claim.claimToken;
|
|
6923
7276
|
if (!leaseId || !claimToken) return void 0;
|
|
6924
|
-
const request = dependencies.request ??
|
|
7277
|
+
const request = dependencies.request ?? leaseHeartbeatApi;
|
|
6925
7278
|
const onError = dependencies.onError ?? ((value) => process.stderr.write(
|
|
6926
7279
|
err(`channel lease heartbeat failed: ${String(value)}
|
|
6927
7280
|
`)
|
|
6928
7281
|
));
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
{
|
|
6944
|
-
|
|
6945
|
-
|
|
7282
|
+
let stop;
|
|
7283
|
+
const beat = async () => {
|
|
7284
|
+
try {
|
|
7285
|
+
return await request(
|
|
7286
|
+
cfg,
|
|
7287
|
+
`/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
|
|
7288
|
+
{
|
|
7289
|
+
method: "POST",
|
|
7290
|
+
body: JSON.stringify({
|
|
7291
|
+
claimToken,
|
|
7292
|
+
tokenVersion: claim.tokenVersion ?? 1
|
|
7293
|
+
})
|
|
7294
|
+
}
|
|
7295
|
+
);
|
|
7296
|
+
} catch (error) {
|
|
7297
|
+
const disposition = classifyLeaseHeartbeatFailure(error);
|
|
7298
|
+
if (disposition === "retry") throw error;
|
|
7299
|
+
stop?.();
|
|
7300
|
+
dependencies.onLeaseTerminal?.(leaseId, error);
|
|
7301
|
+
if (disposition === "auth") {
|
|
7302
|
+
dependencies.onAuthExpired?.(error);
|
|
7303
|
+
return void 0;
|
|
7304
|
+
}
|
|
7305
|
+
if (disposition === "terminal")
|
|
7306
|
+
onError(leaseHeartbeatStoppedLine(leaseId, error));
|
|
7307
|
+
return void 0;
|
|
6946
7308
|
}
|
|
6947
|
-
|
|
7309
|
+
};
|
|
7310
|
+
stop = startLeaseHeartbeat(beat, onError, intervalMilliseconds, {
|
|
7311
|
+
setInterval: dependencies.setInterval,
|
|
7312
|
+
clearInterval: dependencies.clearInterval
|
|
7313
|
+
});
|
|
7314
|
+
return stop;
|
|
6948
7315
|
}
|
|
6949
|
-
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
7316
|
+
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
|
|
6950
7317
|
const stops = /* @__PURE__ */ new Map();
|
|
7318
|
+
let authExpired = false;
|
|
6951
7319
|
const intervalMilliseconds = Math.max(
|
|
6952
7320
|
1e3,
|
|
6953
7321
|
Math.min(3e4, Math.floor(taskLeaseTtlSeconds * 1e3 / 4))
|
|
@@ -6959,10 +7327,30 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
|
6959
7327
|
const stop = startChannelTaskLeaseHeartbeat(
|
|
6960
7328
|
cfg,
|
|
6961
7329
|
claim,
|
|
6962
|
-
intervalMilliseconds
|
|
7330
|
+
intervalMilliseconds,
|
|
7331
|
+
{
|
|
7332
|
+
...dependencies,
|
|
7333
|
+
// The beat has already cancelled its own timer; drop the dead entry so a
|
|
7334
|
+
// later re-claim of the same lease id can start a fresh beat, and so the
|
|
7335
|
+
// map does not accumulate stopped leases for the life of the channel.
|
|
7336
|
+
onLeaseTerminal: (id) => {
|
|
7337
|
+
stops.delete(id);
|
|
7338
|
+
},
|
|
7339
|
+
// A dead credential fails every lease at once, so N held leases would
|
|
7340
|
+
// otherwise raise this N times in the same tick. Latch it: stop every beat
|
|
7341
|
+
// (not just the one that noticed), then escalate exactly once.
|
|
7342
|
+
onAuthExpired: (error) => {
|
|
7343
|
+
if (authExpired) return;
|
|
7344
|
+
authExpired = true;
|
|
7345
|
+
for (const cancel of stops.values()) cancel();
|
|
7346
|
+
stops.clear();
|
|
7347
|
+
dependencies.onAuthExpired?.(error);
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
6963
7350
|
);
|
|
6964
7351
|
if (stop) stops.set(leaseId, stop);
|
|
6965
7352
|
},
|
|
7353
|
+
activeLeaseIds: () => [...stops.keys()],
|
|
6966
7354
|
stop: () => {
|
|
6967
7355
|
for (const stop of stops.values()) stop();
|
|
6968
7356
|
stops.clear();
|
|
@@ -7024,7 +7412,7 @@ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {})
|
|
|
7024
7412
|
}
|
|
7025
7413
|
function readMcpConfig(path) {
|
|
7026
7414
|
if (!existsSync9(path)) return {};
|
|
7027
|
-
const raw =
|
|
7415
|
+
const raw = readFileSync9(path, "utf8");
|
|
7028
7416
|
if (!raw.trim()) return {};
|
|
7029
7417
|
try {
|
|
7030
7418
|
return JSON.parse(raw);
|
|
@@ -7051,15 +7439,50 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
|
7051
7439
|
await conn.start();
|
|
7052
7440
|
return conn;
|
|
7053
7441
|
}
|
|
7054
|
-
function holdOpen(conn) {
|
|
7442
|
+
function holdOpen(conn, until) {
|
|
7055
7443
|
return new Promise((resolve9) => {
|
|
7056
7444
|
const stop = () => {
|
|
7057
7445
|
void conn.stop().finally(resolve9);
|
|
7058
7446
|
};
|
|
7059
7447
|
process.on("SIGINT", stop);
|
|
7060
7448
|
process.on("SIGTERM", stop);
|
|
7449
|
+
void until?.then(stop);
|
|
7061
7450
|
});
|
|
7062
7451
|
}
|
|
7452
|
+
async function holdChannelOpen(conn, released, teardown) {
|
|
7453
|
+
try {
|
|
7454
|
+
await holdOpen(conn, released);
|
|
7455
|
+
} finally {
|
|
7456
|
+
let firstError;
|
|
7457
|
+
let failed = false;
|
|
7458
|
+
for (const step of teardown) {
|
|
7459
|
+
try {
|
|
7460
|
+
await step();
|
|
7461
|
+
} catch (error) {
|
|
7462
|
+
if (!failed) {
|
|
7463
|
+
failed = true;
|
|
7464
|
+
firstError = error;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
if (failed) throw firstError;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
function createAuthExpiredExit() {
|
|
7472
|
+
let release;
|
|
7473
|
+
const released = new Promise((resolve9) => {
|
|
7474
|
+
release = resolve9;
|
|
7475
|
+
});
|
|
7476
|
+
return {
|
|
7477
|
+
onAuthExpired: (error) => {
|
|
7478
|
+
process.stderr.write(err(`${authExpiredLine(error)}
|
|
7479
|
+
`));
|
|
7480
|
+
process.exitCode = 1;
|
|
7481
|
+
release();
|
|
7482
|
+
},
|
|
7483
|
+
released
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7063
7486
|
function parseEvent(payload) {
|
|
7064
7487
|
let data = payload;
|
|
7065
7488
|
if (typeof payload === "string") {
|
|
@@ -7114,96 +7537,10 @@ function str(v) {
|
|
|
7114
7537
|
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
7115
7538
|
}
|
|
7116
7539
|
|
|
7117
|
-
// src/commands/chat.ts
|
|
7118
|
-
function registerChat(program2) {
|
|
7119
|
-
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
7120
|
-
chat.addHelpText(
|
|
7121
|
-
"after",
|
|
7122
|
-
`
|
|
7123
|
-
Examples:
|
|
7124
|
-
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
7125
|
-
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
7126
|
-
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
7127
|
-
$ sechroom chat messages --surface slack
|
|
7128
|
-
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
7129
|
-
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
7130
|
-
);
|
|
7131
|
-
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
|
|
7132
|
-
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
7133
|
-
const json = Boolean(cmd.optsWithGlobals().json);
|
|
7134
|
-
const cfg = resolveConfig(globals);
|
|
7135
|
-
const data = await runApi("Sending message", async () => {
|
|
7136
|
-
const client = await makeClient(cfg);
|
|
7137
|
-
return client.POST("/chat/channel-messages/{surface}", {
|
|
7138
|
-
params: { path: { surface: String(surface) } },
|
|
7139
|
-
body: {
|
|
7140
|
-
channelId,
|
|
7141
|
-
text: text2,
|
|
7142
|
-
guildId: opts.guild ?? null,
|
|
7143
|
-
attachedMemoryId: opts.memory ?? null,
|
|
7144
|
-
trackReplies: opts.track,
|
|
7145
|
-
parentMessage: opts.parent ?? null,
|
|
7146
|
-
source: opts.source,
|
|
7147
|
-
as: opts.as
|
|
7148
|
-
}
|
|
7149
|
-
});
|
|
7150
|
-
});
|
|
7151
|
-
if (!data.ok) {
|
|
7152
|
-
if (json) {
|
|
7153
|
-
emit(data, true);
|
|
7154
|
-
} else {
|
|
7155
|
-
process.stderr.write(
|
|
7156
|
-
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
7157
|
-
`
|
|
7158
|
-
);
|
|
7159
|
-
}
|
|
7160
|
-
process.exit(1);
|
|
7161
|
-
}
|
|
7162
|
-
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
7163
|
-
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
7164
|
-
});
|
|
7165
|
-
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
7166
|
-
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
7167
|
-
const cfg = resolveConfig(globals);
|
|
7168
|
-
const data = await runApi("Fetching messages", async () => {
|
|
7169
|
-
const client = await makeClient(cfg);
|
|
7170
|
-
return client.GET("/chat/channel-messages/{surface}", {
|
|
7171
|
-
params: { path: { surface: String(surface) } }
|
|
7172
|
-
});
|
|
7173
|
-
});
|
|
7174
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
7175
|
-
});
|
|
7176
|
-
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
7177
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
7178
|
-
const data = await runApi("Fetching replies", async () => {
|
|
7179
|
-
const client = await makeClient(cfg);
|
|
7180
|
-
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
7181
|
-
params: { path: { id: messageId } }
|
|
7182
|
-
});
|
|
7183
|
-
});
|
|
7184
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
7185
|
-
});
|
|
7186
|
-
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
7187
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
7188
|
-
const data = await runApi("Stopping reply tracking", async () => {
|
|
7189
|
-
const client = await makeClient(cfg);
|
|
7190
|
-
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
7191
|
-
params: { path: { id: messageId } },
|
|
7192
|
-
body: {}
|
|
7193
|
-
});
|
|
7194
|
-
});
|
|
7195
|
-
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
7196
|
-
});
|
|
7197
|
-
}
|
|
7198
|
-
|
|
7199
|
-
// src/commands/checkpoint.ts
|
|
7200
|
-
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
7201
|
-
import { dirname as dirname14, join as join16 } from "path";
|
|
7202
|
-
|
|
7203
7540
|
// src/commands/hook.ts
|
|
7204
7541
|
import { createHash as createHash4 } from "crypto";
|
|
7205
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as
|
|
7206
|
-
import { dirname as dirname13, join as
|
|
7542
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
7543
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
7207
7544
|
|
|
7208
7545
|
// src/commands/lane-commit-hook.ts
|
|
7209
7546
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -7211,7 +7548,7 @@ import {
|
|
|
7211
7548
|
chmodSync,
|
|
7212
7549
|
existsSync as existsSync10,
|
|
7213
7550
|
mkdirSync as mkdirSync11,
|
|
7214
|
-
readFileSync as
|
|
7551
|
+
readFileSync as readFileSync10,
|
|
7215
7552
|
renameSync,
|
|
7216
7553
|
unlinkSync,
|
|
7217
7554
|
writeFileSync as writeFileSync10
|
|
@@ -7232,7 +7569,7 @@ function resolveCheckoutLane(start) {
|
|
|
7232
7569
|
return pin ? applyWorktreeLaneSuffix(pin, start) : void 0;
|
|
7233
7570
|
}
|
|
7234
7571
|
function appendLaneTrailer(messagePath, lane) {
|
|
7235
|
-
const original =
|
|
7572
|
+
const original = readFileSync10(messagePath, "utf8").replace(/\r\n/g, "\n");
|
|
7236
7573
|
const lines = original.split("\n");
|
|
7237
7574
|
const scissorsIndex = lines.findIndex(
|
|
7238
7575
|
(line) => /^#\s*-+\s*>8\s*-+\s*$/.test(line)
|
|
@@ -7304,7 +7641,7 @@ function resolveHookPath(root, hookName) {
|
|
|
7304
7641
|
function removeLegacyPrepareCommitMsgLeg(root) {
|
|
7305
7642
|
const path = resolveHookPath(root, LEGACY_HOOK_NAME);
|
|
7306
7643
|
if (!existsSync10(path)) return;
|
|
7307
|
-
const current =
|
|
7644
|
+
const current = readFileSync10(path, "utf8");
|
|
7308
7645
|
const pattern = managedBlockPattern();
|
|
7309
7646
|
if (!pattern.test(current)) return;
|
|
7310
7647
|
const next = current.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
@@ -7324,7 +7661,7 @@ function installLaneCommitHook(start) {
|
|
|
7324
7661
|
}).trim();
|
|
7325
7662
|
removeLegacyPrepareCommitMsgLeg(root);
|
|
7326
7663
|
const path = resolveHookPath(root, HOOK_NAME);
|
|
7327
|
-
const current = existsSync10(path) ?
|
|
7664
|
+
const current = existsSync10(path) ? readFileSync10(path, "utf8") : "";
|
|
7328
7665
|
let next;
|
|
7329
7666
|
if (current && !isShellHook(current)) {
|
|
7330
7667
|
const incumbentPath = nextIncumbentPath(path);
|
|
@@ -7347,25 +7684,25 @@ function installLaneCommitHook(start) {
|
|
|
7347
7684
|
}
|
|
7348
7685
|
|
|
7349
7686
|
// 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
|
|
7687
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
7688
|
+
import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
7689
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
7353
7690
|
|
|
7354
7691
|
// src/setup/skill-composition-materialise.ts
|
|
7355
|
-
import { createHash as createHash3, randomUUID } from "crypto";
|
|
7692
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
7356
7693
|
import {
|
|
7357
7694
|
existsSync as existsSync11,
|
|
7358
7695
|
mkdirSync as mkdirSync12,
|
|
7359
|
-
readdirSync as
|
|
7360
|
-
readFileSync as
|
|
7696
|
+
readdirSync as readdirSync3,
|
|
7697
|
+
readFileSync as readFileSync11,
|
|
7361
7698
|
renameSync as renameSync2,
|
|
7362
7699
|
rmdirSync,
|
|
7363
|
-
rmSync as
|
|
7364
|
-
statSync as
|
|
7700
|
+
rmSync as rmSync6,
|
|
7701
|
+
statSync as statSync3,
|
|
7365
7702
|
writeFileSync as writeFileSync11
|
|
7366
7703
|
} from "fs";
|
|
7367
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, join as
|
|
7368
|
-
var COMPILED_SKILL_LOCK =
|
|
7704
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, join as join14, relative, resolve as resolve5 } from "path";
|
|
7705
|
+
var COMPILED_SKILL_LOCK = join14(
|
|
7369
7706
|
".sechroom",
|
|
7370
7707
|
"compiled-skill-materialisation.json"
|
|
7371
7708
|
);
|
|
@@ -7430,7 +7767,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7430
7767
|
desired.delete(key);
|
|
7431
7768
|
}
|
|
7432
7769
|
}
|
|
7433
|
-
const lockPath =
|
|
7770
|
+
const lockPath = join14(cwd, COMPILED_SKILL_LOCK);
|
|
7434
7771
|
const previous = readLock(lockPath, destinations);
|
|
7435
7772
|
const next = {
|
|
7436
7773
|
version: 2,
|
|
@@ -7441,7 +7778,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7441
7778
|
const path = skillPath(value.skillsRoot, value.skill.name);
|
|
7442
7779
|
const directory = dirname11(path);
|
|
7443
7780
|
const prior = previous.entries[key];
|
|
7444
|
-
const existing = existsSync11(path) ?
|
|
7781
|
+
const existing = existsSync11(path) ? readFileSync11(path, "utf8") : void 0;
|
|
7445
7782
|
const existingHash = existing === void 0 ? void 0 : sha256(existing);
|
|
7446
7783
|
const owned = prior !== void 0 && prior.skillsRoot === value.skillsRoot && existing !== void 0 && (existingHash === prior.contentHash || existingHash === prior.previousContentHash);
|
|
7447
7784
|
const recoverableMissing = prior !== void 0 && existing === void 0;
|
|
@@ -7489,7 +7826,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7489
7826
|
};
|
|
7490
7827
|
writeLock(lockPath, next);
|
|
7491
7828
|
mkdirSync12(directory, { recursive: true });
|
|
7492
|
-
if (existingHash !== void 0 && (!existsSync11(path) || sha256(
|
|
7829
|
+
if (existingHash !== void 0 && (!existsSync11(path) || sha256(readFileSync11(path, "utf8")) !== existingHash)) {
|
|
7493
7830
|
delete next.entries[key];
|
|
7494
7831
|
writeLock(lockPath, next);
|
|
7495
7832
|
items.push({
|
|
@@ -7572,7 +7909,7 @@ function entryKey(target, name, skillsRoot) {
|
|
|
7572
7909
|
return `${target}:${sha256(skillsRoot)}:${name}`;
|
|
7573
7910
|
}
|
|
7574
7911
|
function skillPath(skillsRoot, name) {
|
|
7575
|
-
return
|
|
7912
|
+
return join14(skillsRoot, name, "SKILL.md");
|
|
7576
7913
|
}
|
|
7577
7914
|
function withFinalNewline(body) {
|
|
7578
7915
|
return body.endsWith("\n") ? body : body + "\n";
|
|
@@ -7587,7 +7924,7 @@ function readLock(path, destinations) {
|
|
|
7587
7924
|
)
|
|
7588
7925
|
);
|
|
7589
7926
|
try {
|
|
7590
|
-
const parsed = JSON.parse(
|
|
7927
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7591
7928
|
if (parsed.version === 2 && parsed.entries && typeof parsed.entries === "object") {
|
|
7592
7929
|
const entries = {};
|
|
7593
7930
|
for (const [key, candidate] of Object.entries(parsed.entries)) {
|
|
@@ -7618,22 +7955,22 @@ function writeAtomic(path, body) {
|
|
|
7618
7955
|
renameSync2(temporary, path);
|
|
7619
7956
|
}
|
|
7620
7957
|
function removeOwnedFile(path, acceptableHashes) {
|
|
7621
|
-
const quarantine = `${path}.sechroom-retire-${process.pid}-${
|
|
7958
|
+
const quarantine = `${path}.sechroom-retire-${process.pid}-${randomUUID2()}`;
|
|
7622
7959
|
try {
|
|
7623
7960
|
renameSync2(path, quarantine);
|
|
7624
7961
|
} catch (error) {
|
|
7625
7962
|
if (error.code === "ENOENT") return "missing";
|
|
7626
7963
|
throw error;
|
|
7627
7964
|
}
|
|
7628
|
-
const quarantinedHash = sha256(
|
|
7965
|
+
const quarantinedHash = sha256(readFileSync11(quarantine, "utf8"));
|
|
7629
7966
|
if (acceptableHashes.includes(quarantinedHash)) {
|
|
7630
|
-
|
|
7967
|
+
rmSync6(quarantine);
|
|
7631
7968
|
return "removed";
|
|
7632
7969
|
}
|
|
7633
7970
|
if (!existsSync11(path)) {
|
|
7634
7971
|
renameSync2(quarantine, path);
|
|
7635
7972
|
} else {
|
|
7636
|
-
renameSync2(quarantine, `${path}.sechroom-preserved-${
|
|
7973
|
+
renameSync2(quarantine, `${path}.sechroom-preserved-${randomUUID2()}`);
|
|
7637
7974
|
}
|
|
7638
7975
|
return "collision";
|
|
7639
7976
|
}
|
|
@@ -7642,7 +7979,7 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7642
7979
|
if (!path) return;
|
|
7643
7980
|
let current = "";
|
|
7644
7981
|
try {
|
|
7645
|
-
current =
|
|
7982
|
+
current = readFileSync11(path, "utf8");
|
|
7646
7983
|
} catch {
|
|
7647
7984
|
}
|
|
7648
7985
|
const withoutOwnedBlock = removeOwnedExcludeBlock(current);
|
|
@@ -7657,14 +7994,14 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7657
7994
|
writeAtomic(path, updated);
|
|
7658
7995
|
}
|
|
7659
7996
|
function gitExcludePath(cwd) {
|
|
7660
|
-
const dotGit =
|
|
7997
|
+
const dotGit = join14(cwd, ".git");
|
|
7661
7998
|
try {
|
|
7662
|
-
if (
|
|
7663
|
-
const pointer =
|
|
7999
|
+
if (statSync3(dotGit).isDirectory()) return join14(dotGit, "info", "exclude");
|
|
8000
|
+
const pointer = readFileSync11(dotGit, "utf8").trim();
|
|
7664
8001
|
if (!pointer.startsWith("gitdir:")) return void 0;
|
|
7665
8002
|
const raw = pointer.slice("gitdir:".length).trim();
|
|
7666
8003
|
const gitDir = isAbsolute2(raw) ? raw : resolve5(cwd, raw);
|
|
7667
|
-
return
|
|
8004
|
+
return join14(gitDir, "info", "exclude");
|
|
7668
8005
|
} catch {
|
|
7669
8006
|
return void 0;
|
|
7670
8007
|
}
|
|
@@ -7681,13 +8018,13 @@ function removeOwnedExcludeBlock(body) {
|
|
|
7681
8018
|
}
|
|
7682
8019
|
function removeDirectoryIfEmpty(path) {
|
|
7683
8020
|
try {
|
|
7684
|
-
if (
|
|
8021
|
+
if (readdirSync3(path).length === 0) rmdirSync(path);
|
|
7685
8022
|
} catch {
|
|
7686
8023
|
}
|
|
7687
8024
|
}
|
|
7688
8025
|
|
|
7689
8026
|
// src/commands/session-context.ts
|
|
7690
|
-
var DYNAMIC_AGENT_CONTEXT_FILE =
|
|
8027
|
+
var DYNAMIC_AGENT_CONTEXT_FILE = join15(".sechroom", "CLAUDE.md");
|
|
7691
8028
|
function checkoutRoot(start) {
|
|
7692
8029
|
const semPath = resolveSemPathForRead(start);
|
|
7693
8030
|
return semPath ? dirname12(dirname12(semPath)) : start;
|
|
@@ -7724,7 +8061,7 @@ function renderSessionContext(result, lane) {
|
|
|
7724
8061
|
}
|
|
7725
8062
|
function writeSessionContext(start, lane, result, options = {}) {
|
|
7726
8063
|
const root = checkoutRoot(start);
|
|
7727
|
-
const path =
|
|
8064
|
+
const path = join15(root, DYNAMIC_AGENT_CONTEXT_FILE);
|
|
7728
8065
|
const skills = materialiseCompiledSkillCompositions(
|
|
7729
8066
|
root,
|
|
7730
8067
|
result.status === "hold" ? {
|
|
@@ -7740,13 +8077,13 @@ function writeSessionContext(start, lane, result, options = {}) {
|
|
|
7740
8077
|
);
|
|
7741
8078
|
const context = renderSessionContext(result, lane);
|
|
7742
8079
|
mkdirSync13(dirname12(path), { recursive: true });
|
|
7743
|
-
const temporaryPath = `${path}.${process.pid}.${
|
|
8080
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
7744
8081
|
try {
|
|
7745
8082
|
writeFileSync12(temporaryPath, context.endsWith("\n") ? context : `${context}
|
|
7746
8083
|
`, "utf8");
|
|
7747
8084
|
renameSync3(temporaryPath, path);
|
|
7748
8085
|
} catch (error) {
|
|
7749
|
-
|
|
8086
|
+
rmSync7(temporaryPath, { force: true });
|
|
7750
8087
|
throw error;
|
|
7751
8088
|
}
|
|
7752
8089
|
return { status: result.status, path, context, skills };
|
|
@@ -7797,12 +8134,23 @@ function resolveLane(flagLane, cwd) {
|
|
|
7797
8134
|
if (!base) return void 0;
|
|
7798
8135
|
return applyWorktreeLaneSuffix(base, start);
|
|
7799
8136
|
}
|
|
7800
|
-
|
|
8137
|
+
function resolveSourceLane(explicit, cwd) {
|
|
8138
|
+
const trimmed = explicit?.trim();
|
|
8139
|
+
if (trimmed) return trimmed;
|
|
8140
|
+
const lane = resolveLane(void 0, cwd);
|
|
8141
|
+
if (!lane) {
|
|
8142
|
+
throw new Error(
|
|
8143
|
+
"no --source and no lane pinned for this checkout \u2014 a record's source must name the lane that wrote it, never a generic value. Pin one with `sechroom lane set --code-lane <id>` (see `sechroom lane`), set SECHROOM_LANE, or pass --source explicitly."
|
|
8144
|
+
);
|
|
8145
|
+
}
|
|
8146
|
+
return lane;
|
|
8147
|
+
}
|
|
8148
|
+
var INTENT_FILE = join16(".sechroom", "continuity.json");
|
|
7801
8149
|
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
7802
8150
|
function resolveIntentPath(start) {
|
|
7803
8151
|
let dir = start;
|
|
7804
8152
|
for (; ; ) {
|
|
7805
|
-
const candidate =
|
|
8153
|
+
const candidate = join16(dir, INTENT_FILE);
|
|
7806
8154
|
if (existsSync12(candidate)) return candidate;
|
|
7807
8155
|
const parent = dirname13(dir);
|
|
7808
8156
|
if (parent === dir) return void 0;
|
|
@@ -7813,7 +8161,7 @@ function readIntent(start) {
|
|
|
7813
8161
|
const path = resolveIntentPath(start);
|
|
7814
8162
|
if (!path) return void 0;
|
|
7815
8163
|
try {
|
|
7816
|
-
return JSON.parse(
|
|
8164
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
7817
8165
|
} catch {
|
|
7818
8166
|
return void 0;
|
|
7819
8167
|
}
|
|
@@ -7833,6 +8181,56 @@ function localDryRunMissingFields(i) {
|
|
|
7833
8181
|
];
|
|
7834
8182
|
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
7835
8183
|
}
|
|
8184
|
+
var SNAPSHOT_LIMITS = {
|
|
8185
|
+
/** objective / state / lastAction / nextAction / resumeInstruction. */
|
|
8186
|
+
bodyFieldLength: 2e3,
|
|
8187
|
+
/** Any single entry in constraints / questions / surfaceMarkers / artifacts. */
|
|
8188
|
+
listItemLength: 500,
|
|
8189
|
+
/** Entries per list. */
|
|
8190
|
+
listLength: 25
|
|
8191
|
+
};
|
|
8192
|
+
function snapshotLimitViolations(i) {
|
|
8193
|
+
const violations = [];
|
|
8194
|
+
const bodyFields = [
|
|
8195
|
+
"objective",
|
|
8196
|
+
"state",
|
|
8197
|
+
"lastAction",
|
|
8198
|
+
"nextAction",
|
|
8199
|
+
"resumeInstruction"
|
|
8200
|
+
];
|
|
8201
|
+
for (const key of bodyFields) {
|
|
8202
|
+
const value = i[key];
|
|
8203
|
+
if (typeof value !== "string") continue;
|
|
8204
|
+
if (value.length > SNAPSHOT_LIMITS.bodyFieldLength) {
|
|
8205
|
+
violations.push(
|
|
8206
|
+
`${key}: ${value.length} characters exceeds the ${SNAPSHOT_LIMITS.bodyFieldLength}-character limit`
|
|
8207
|
+
);
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
const lists = [
|
|
8211
|
+
"constraints",
|
|
8212
|
+
"questions",
|
|
8213
|
+
"surfaceMarkers",
|
|
8214
|
+
"artifacts"
|
|
8215
|
+
];
|
|
8216
|
+
for (const key of lists) {
|
|
8217
|
+
const items = i[key];
|
|
8218
|
+
if (!Array.isArray(items)) continue;
|
|
8219
|
+
if (items.length > SNAPSHOT_LIMITS.listLength) {
|
|
8220
|
+
violations.push(
|
|
8221
|
+
`${key}: ${items.length} entries exceeds the ${SNAPSHOT_LIMITS.listLength}-entry limit`
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
items.forEach((item, index) => {
|
|
8225
|
+
if (typeof item === "string" && item.length > SNAPSHOT_LIMITS.listItemLength) {
|
|
8226
|
+
violations.push(
|
|
8227
|
+
`${key}[${index}]: ${item.length} characters exceeds the ${SNAPSHOT_LIMITS.listItemLength}-character limit`
|
|
8228
|
+
);
|
|
8229
|
+
}
|
|
8230
|
+
});
|
|
8231
|
+
}
|
|
8232
|
+
return violations;
|
|
8233
|
+
}
|
|
7836
8234
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
7837
8235
|
const lane = resolveLane(laneFlag, cwd);
|
|
7838
8236
|
if (!lane) return false;
|
|
@@ -7867,14 +8265,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7867
8265
|
}
|
|
7868
8266
|
function ledgerPath(start) {
|
|
7869
8267
|
const intent = resolveIntentPath(start);
|
|
7870
|
-
const dir = intent ? dirname13(intent) :
|
|
7871
|
-
return
|
|
8268
|
+
const dir = intent ? dirname13(intent) : join16(start, ".sechroom");
|
|
8269
|
+
return join16(dir, ".checkpoint-state.json");
|
|
7872
8270
|
}
|
|
7873
8271
|
function readLedger(start) {
|
|
7874
8272
|
try {
|
|
7875
8273
|
const p = ledgerPath(start);
|
|
7876
8274
|
if (!existsSync12(p)) return {};
|
|
7877
|
-
return JSON.parse(
|
|
8275
|
+
return JSON.parse(readFileSync12(p, "utf8"));
|
|
7878
8276
|
} catch {
|
|
7879
8277
|
return {};
|
|
7880
8278
|
}
|
|
@@ -7905,7 +8303,7 @@ function unchangedSinceLastPush(start, intent) {
|
|
|
7905
8303
|
const path = resolveIntentPath(start);
|
|
7906
8304
|
if (path && ledger.lastMtimeMs != null) {
|
|
7907
8305
|
try {
|
|
7908
|
-
if (
|
|
8306
|
+
if (statSync4(path).mtimeMs <= ledger.lastMtimeMs) return true;
|
|
7909
8307
|
} catch {
|
|
7910
8308
|
}
|
|
7911
8309
|
}
|
|
@@ -7917,7 +8315,7 @@ function recordPush(start, intent) {
|
|
|
7917
8315
|
const path = resolveIntentPath(start);
|
|
7918
8316
|
let mtimeMs;
|
|
7919
8317
|
try {
|
|
7920
|
-
if (path) mtimeMs =
|
|
8318
|
+
if (path) mtimeMs = statSync4(path).mtimeMs;
|
|
7921
8319
|
} catch {
|
|
7922
8320
|
mtimeMs = void 0;
|
|
7923
8321
|
}
|
|
@@ -8147,7 +8545,96 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
8147
8545
|
});
|
|
8148
8546
|
}
|
|
8149
8547
|
|
|
8548
|
+
// src/commands/chat.ts
|
|
8549
|
+
function registerChat(program2) {
|
|
8550
|
+
const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
|
|
8551
|
+
chat.addHelpText(
|
|
8552
|
+
"after",
|
|
8553
|
+
`
|
|
8554
|
+
Examples:
|
|
8555
|
+
$ sechroom chat send C0123456789 "deploy is green" --surface slack
|
|
8556
|
+
$ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
|
|
8557
|
+
$ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
|
|
8558
|
+
$ sechroom chat messages --surface slack
|
|
8559
|
+
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
8560
|
+
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
8561
|
+
);
|
|
8562
|
+
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer; default: this checkout's pinned code-lane)").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
|
|
8563
|
+
const source = resolveSourceLane(opts.source);
|
|
8564
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8565
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
8566
|
+
const cfg = resolveConfig(globals);
|
|
8567
|
+
const data = await runApi("Sending message", async () => {
|
|
8568
|
+
const client = await makeClient(cfg);
|
|
8569
|
+
return client.POST("/chat/channel-messages/{surface}", {
|
|
8570
|
+
params: { path: { surface: String(surface) } },
|
|
8571
|
+
body: {
|
|
8572
|
+
channelId,
|
|
8573
|
+
text: text2,
|
|
8574
|
+
guildId: opts.guild ?? null,
|
|
8575
|
+
attachedMemoryId: opts.memory ?? null,
|
|
8576
|
+
trackReplies: opts.track,
|
|
8577
|
+
parentMessage: opts.parent ?? null,
|
|
8578
|
+
source,
|
|
8579
|
+
as: opts.as
|
|
8580
|
+
}
|
|
8581
|
+
});
|
|
8582
|
+
});
|
|
8583
|
+
if (!data.ok) {
|
|
8584
|
+
if (json) {
|
|
8585
|
+
emit(data, true);
|
|
8586
|
+
} else {
|
|
8587
|
+
process.stderr.write(
|
|
8588
|
+
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
8589
|
+
`
|
|
8590
|
+
);
|
|
8591
|
+
}
|
|
8592
|
+
process.exit(1);
|
|
8593
|
+
}
|
|
8594
|
+
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
8595
|
+
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
8596
|
+
});
|
|
8597
|
+
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
8598
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
8599
|
+
const cfg = resolveConfig(globals);
|
|
8600
|
+
const data = await runApi("Fetching messages", async () => {
|
|
8601
|
+
const client = await makeClient(cfg);
|
|
8602
|
+
return client.GET("/chat/channel-messages/{surface}", {
|
|
8603
|
+
params: { path: { surface: String(surface) } }
|
|
8604
|
+
});
|
|
8605
|
+
});
|
|
8606
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8607
|
+
});
|
|
8608
|
+
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
8609
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8610
|
+
const data = await runApi("Fetching replies", async () => {
|
|
8611
|
+
const client = await makeClient(cfg);
|
|
8612
|
+
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
8613
|
+
params: { path: { id: messageId } }
|
|
8614
|
+
});
|
|
8615
|
+
});
|
|
8616
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
8617
|
+
});
|
|
8618
|
+
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
8619
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8620
|
+
const data = await runApi("Stopping reply tracking", async () => {
|
|
8621
|
+
const client = await makeClient(cfg);
|
|
8622
|
+
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
8623
|
+
params: { path: { id: messageId } },
|
|
8624
|
+
body: {}
|
|
8625
|
+
});
|
|
8626
|
+
});
|
|
8627
|
+
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
8628
|
+
});
|
|
8629
|
+
}
|
|
8630
|
+
|
|
8150
8631
|
// src/commands/checkpoint.ts
|
|
8632
|
+
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
8633
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
8634
|
+
var CHECKPOINT_UNCHANGED_EXIT_CODE = 3;
|
|
8635
|
+
function checkpointUnchangedLine(snapshotId) {
|
|
8636
|
+
return `${style.bold("=")} unchanged ${style.dim(`(${snapshotId} already current)`)} \u2014 no new snapshot; the local file was left as-is`;
|
|
8637
|
+
}
|
|
8151
8638
|
function registerCheckpoint(program2) {
|
|
8152
8639
|
program2.command("checkpoint").description(
|
|
8153
8640
|
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
@@ -8195,6 +8682,13 @@ Examples:
|
|
|
8195
8682
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
8196
8683
|
);
|
|
8197
8684
|
}
|
|
8685
|
+
const overLimit = snapshotLimitViolations(merged);
|
|
8686
|
+
if (overLimit.length > 0) {
|
|
8687
|
+
fail(
|
|
8688
|
+
`snapshot exceeds the field limits \u2014 trim and retry:
|
|
8689
|
+
${overLimit.join("\n ")}`
|
|
8690
|
+
);
|
|
8691
|
+
}
|
|
8198
8692
|
const scope = merged.scope ?? "session";
|
|
8199
8693
|
const body = {
|
|
8200
8694
|
laneId: lane,
|
|
@@ -8241,7 +8735,17 @@ Examples:
|
|
|
8241
8735
|
const client = await makeClient(cfg);
|
|
8242
8736
|
return client.POST("/continuity/snapshots", { body });
|
|
8243
8737
|
});
|
|
8244
|
-
const
|
|
8738
|
+
const previousSnapshotId = base.lastSnapshotId;
|
|
8739
|
+
if (previousSnapshotId && data.snapshotId === previousSnapshotId) {
|
|
8740
|
+
process.exitCode = CHECKPOINT_UNCHANGED_EXIT_CODE;
|
|
8741
|
+
if (json) {
|
|
8742
|
+
emit({ snapshotId: data.snapshotId, lane, scope, unchanged: true, file: null }, true);
|
|
8743
|
+
return;
|
|
8744
|
+
}
|
|
8745
|
+
process.stdout.write(checkpointUnchangedLine(data.snapshotId) + "\n");
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8748
|
+
const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
|
|
8245
8749
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
8246
8750
|
mkdirSync15(dirname14(path), { recursive: true });
|
|
8247
8751
|
writeFileSync14(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
@@ -8258,7 +8762,7 @@ Examples:
|
|
|
8258
8762
|
}
|
|
8259
8763
|
|
|
8260
8764
|
// src/commands/close.ts
|
|
8261
|
-
import { readFileSync as
|
|
8765
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
8262
8766
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
8263
8767
|
function registerClose(program2) {
|
|
8264
8768
|
program2.command("close").description(
|
|
@@ -8278,7 +8782,7 @@ function registerClose(program2) {
|
|
|
8278
8782
|
).option(
|
|
8279
8783
|
"--to-version <n>",
|
|
8280
8784
|
"Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
|
|
8281
|
-
).option("--source <source>", "Source / lane stamp
|
|
8785
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").addHelpText(
|
|
8282
8786
|
"after",
|
|
8283
8787
|
`
|
|
8284
8788
|
Examples:
|
|
@@ -8290,6 +8794,7 @@ Examples:
|
|
|
8290
8794
|
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
8291
8795
|
--title "done" --file ./closeout.md`
|
|
8292
8796
|
).action(async (opts, cmd) => {
|
|
8797
|
+
const source = resolveSourceLane(opts.source);
|
|
8293
8798
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
8294
8799
|
const json = cmd.optsWithGlobals().json;
|
|
8295
8800
|
const verdict = String(opts.verdict);
|
|
@@ -8299,7 +8804,7 @@ Examples:
|
|
|
8299
8804
|
);
|
|
8300
8805
|
let bodyText;
|
|
8301
8806
|
try {
|
|
8302
|
-
bodyText = opts.file ?
|
|
8807
|
+
bodyText = opts.file ? readFileSync13(opts.file, "utf8") : readFileSync13(0, "utf8");
|
|
8303
8808
|
} catch {
|
|
8304
8809
|
fail(
|
|
8305
8810
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -8349,7 +8854,7 @@ Examples:
|
|
|
8349
8854
|
type: "reference",
|
|
8350
8855
|
content: "{}",
|
|
8351
8856
|
confidence: 1,
|
|
8352
|
-
source
|
|
8857
|
+
source,
|
|
8353
8858
|
archetype: "Document",
|
|
8354
8859
|
title: opts.title,
|
|
8355
8860
|
tags,
|
|
@@ -8406,7 +8911,7 @@ Examples:
|
|
|
8406
8911
|
params: { path: { memoryId: opts.task } },
|
|
8407
8912
|
body: {
|
|
8408
8913
|
memoryId: opts.task,
|
|
8409
|
-
source
|
|
8914
|
+
source,
|
|
8410
8915
|
tags: nextTags
|
|
8411
8916
|
}
|
|
8412
8917
|
})
|
|
@@ -8770,7 +9275,7 @@ Examples:
|
|
|
8770
9275
|
);
|
|
8771
9276
|
});
|
|
8772
9277
|
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)"
|
|
9278
|
+
"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
9279
|
).requiredOption(
|
|
8775
9280
|
"--file <path>",
|
|
8776
9281
|
"JSON file containing { tasks, gates? } (the AppendTasksInput shape); use - for stdin"
|
|
@@ -9190,13 +9695,13 @@ function registerGitHub(program2) {
|
|
|
9190
9695
|
}
|
|
9191
9696
|
|
|
9192
9697
|
// src/commands/herdr.ts
|
|
9193
|
-
import { readFileSync as
|
|
9698
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
9194
9699
|
import { basename as basename3 } from "path";
|
|
9195
9700
|
|
|
9196
9701
|
// src/herdr/client.ts
|
|
9197
9702
|
import { createConnection as createConnection2 } from "net";
|
|
9198
9703
|
import { homedir as homedir5 } from "os";
|
|
9199
|
-
import { join as
|
|
9704
|
+
import { join as join18 } from "path";
|
|
9200
9705
|
var DEFAULT_HERDR_SOCKET_RELATIVE = ".config/herdr/herdr.sock";
|
|
9201
9706
|
var HerdrUnreachableError = class extends Error {
|
|
9202
9707
|
constructor(socketPath, reason) {
|
|
@@ -9221,7 +9726,7 @@ function resolveSocketPath(flag, env = process.env, home = homedir5()) {
|
|
|
9221
9726
|
if (fromFlag) return fromFlag;
|
|
9222
9727
|
const fromEnv = env.HERDR_SOCKET?.trim();
|
|
9223
9728
|
if (fromEnv) return fromEnv;
|
|
9224
|
-
return
|
|
9729
|
+
return join18(home, DEFAULT_HERDR_SOCKET_RELATIVE);
|
|
9225
9730
|
}
|
|
9226
9731
|
function expandTarget(target) {
|
|
9227
9732
|
const trimmed = target.trim();
|
|
@@ -9561,7 +10066,7 @@ function parseSource(value, fallback = DEFAULT_READ_SOURCE) {
|
|
|
9561
10066
|
}
|
|
9562
10067
|
return match;
|
|
9563
10068
|
}
|
|
9564
|
-
function resolveSendText(textArgs, useStdin, readStdin5 = () =>
|
|
10069
|
+
function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync14(0, "utf8")) {
|
|
9565
10070
|
if (useStdin) {
|
|
9566
10071
|
if (textArgs.length > 0) {
|
|
9567
10072
|
throw new Error(
|
|
@@ -10301,11 +10806,11 @@ Examples:
|
|
|
10301
10806
|
}
|
|
10302
10807
|
|
|
10303
10808
|
// src/commands/memory.ts
|
|
10304
|
-
import { readFileSync as
|
|
10809
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
10305
10810
|
|
|
10306
10811
|
// src/commands/memory-import.ts
|
|
10307
|
-
import { readdirSync as
|
|
10308
|
-
import { basename as basename4, join as
|
|
10812
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync, statSync as statSync5 } from "fs";
|
|
10813
|
+
import { basename as basename4, join as join19, resolve as resolve6 } from "path";
|
|
10309
10814
|
var MARKDOWN_RE = /\.(md|markdown)$/i;
|
|
10310
10815
|
function isMarkdownPath(path) {
|
|
10311
10816
|
return MARKDOWN_RE.test(path);
|
|
@@ -10332,18 +10837,18 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10332
10837
|
files.push(path);
|
|
10333
10838
|
};
|
|
10334
10839
|
const walk = (dir) => {
|
|
10335
|
-
const entries =
|
|
10840
|
+
const entries = readdirSync4(dir, { withFileTypes: true }).sort(
|
|
10336
10841
|
(a, b) => a.name.localeCompare(b.name)
|
|
10337
10842
|
);
|
|
10338
10843
|
for (const entry of entries) {
|
|
10339
10844
|
if (entry.name.startsWith(".")) continue;
|
|
10340
|
-
const child =
|
|
10845
|
+
const child = join19(dir, entry.name);
|
|
10341
10846
|
let isDirectory = entry.isDirectory();
|
|
10342
10847
|
let isFile = entry.isFile();
|
|
10343
10848
|
if (entry.isSymbolicLink()) {
|
|
10344
10849
|
let target;
|
|
10345
10850
|
try {
|
|
10346
|
-
target =
|
|
10851
|
+
target = statSync5(child);
|
|
10347
10852
|
} catch {
|
|
10348
10853
|
skipped.push({ path: child, reason: "broken symlink" });
|
|
10349
10854
|
continue;
|
|
@@ -10381,7 +10886,7 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10381
10886
|
for (const input of inputs) {
|
|
10382
10887
|
let isDirectory;
|
|
10383
10888
|
try {
|
|
10384
|
-
isDirectory =
|
|
10889
|
+
isDirectory = statSync5(input).isDirectory();
|
|
10385
10890
|
} catch {
|
|
10386
10891
|
missing.push(input);
|
|
10387
10892
|
continue;
|
|
@@ -10397,7 +10902,7 @@ function buildImportPlan(collected) {
|
|
|
10397
10902
|
for (const path of collected.files) {
|
|
10398
10903
|
let text2;
|
|
10399
10904
|
try {
|
|
10400
|
-
text2 =
|
|
10905
|
+
text2 = readFileSync15(path, "utf8");
|
|
10401
10906
|
} catch (error) {
|
|
10402
10907
|
throw new Error(
|
|
10403
10908
|
`couldn't read ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -10498,7 +11003,7 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10498
11003
|
}
|
|
10499
11004
|
if (textOpt != null) return { text: textOpt, defaultTitle: null };
|
|
10500
11005
|
const fromStdin = fileOpt === "-";
|
|
10501
|
-
const text2 = fromStdin ?
|
|
11006
|
+
const text2 = fromStdin ? readFileSync16(0, "utf8") : readFileSync16(String(fileOpt), "utf8");
|
|
10502
11007
|
if (text2.trim().length === 0) {
|
|
10503
11008
|
fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
|
|
10504
11009
|
}
|
|
@@ -10552,7 +11057,8 @@ Examples:
|
|
|
10552
11057
|
"--owner-type <ownerType>",
|
|
10553
11058
|
"Workspace | Project | Unfiled",
|
|
10554
11059
|
"Unfiled"
|
|
10555
|
-
).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp
|
|
11060
|
+
).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
|
|
11061
|
+
const source = resolveSourceLane(opts.source);
|
|
10556
11062
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10557
11063
|
const { text: text2, defaultTitle } = resolveCreateBody(opts.text, opts.file);
|
|
10558
11064
|
const title = opts.title ?? defaultTitle;
|
|
@@ -10565,7 +11071,7 @@ Examples:
|
|
|
10565
11071
|
type: opts.type,
|
|
10566
11072
|
content: "{}",
|
|
10567
11073
|
confidence: Number(opts.confidence),
|
|
10568
|
-
source
|
|
11074
|
+
source,
|
|
10569
11075
|
archetype: "Document",
|
|
10570
11076
|
title: title ?? null,
|
|
10571
11077
|
tags: opts.tag ?? null,
|
|
@@ -10594,7 +11100,8 @@ Examples:
|
|
|
10594
11100
|
"--create-workspace",
|
|
10595
11101
|
"Create the --workspace when its name matches nothing",
|
|
10596
11102
|
false
|
|
10597
|
-
).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp
|
|
11103
|
+
).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
|
|
11104
|
+
const source = resolveSourceLane(opts.source);
|
|
10598
11105
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10599
11106
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
10600
11107
|
const dryRun = Boolean(opts.dryRun);
|
|
@@ -10707,7 +11214,7 @@ ${plan.rows.map(
|
|
|
10707
11214
|
workspaceId: workspace.id,
|
|
10708
11215
|
type: opts.type,
|
|
10709
11216
|
tags: opts.tag ?? null,
|
|
10710
|
-
source
|
|
11217
|
+
source,
|
|
10711
11218
|
confidence: Number(opts.confidence)
|
|
10712
11219
|
},
|
|
10713
11220
|
ports,
|
|
@@ -10829,7 +11336,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10829
11336
|
"--replace-all",
|
|
10830
11337
|
"Replace every occurrence (default: first only)",
|
|
10831
11338
|
false
|
|
10832
|
-
).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp
|
|
11339
|
+
).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11340
|
+
const source = resolveSourceLane(opts.source);
|
|
10833
11341
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10834
11342
|
const data = await runApi("Editing memory text", async () => {
|
|
10835
11343
|
const client = await makeClient(cfg);
|
|
@@ -10841,7 +11349,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10841
11349
|
newText: opts.new,
|
|
10842
11350
|
replaceAll: Boolean(opts.replaceAll),
|
|
10843
11351
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
10844
|
-
source
|
|
11352
|
+
source
|
|
10845
11353
|
}
|
|
10846
11354
|
});
|
|
10847
11355
|
});
|
|
@@ -10853,7 +11361,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10853
11361
|
});
|
|
10854
11362
|
memory.command("edit-text-batch <memoryId>").description(
|
|
10855
11363
|
"Apply many find/replace edits (POST /memories/{memoryId}/edit-text-batch)"
|
|
10856
|
-
).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp
|
|
11364
|
+
).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11365
|
+
const source = resolveSourceLane(opts.source);
|
|
10857
11366
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10858
11367
|
const replaceAll = Boolean(opts.replaceAll);
|
|
10859
11368
|
const edits = opts.edit.map((spec) => {
|
|
@@ -10879,7 +11388,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10879
11388
|
memoryId,
|
|
10880
11389
|
edits,
|
|
10881
11390
|
regenerateFiling: Boolean(opts.regenerateFiling),
|
|
10882
|
-
source
|
|
11391
|
+
source
|
|
10883
11392
|
}
|
|
10884
11393
|
});
|
|
10885
11394
|
});
|
|
@@ -10904,7 +11413,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10904
11413
|
"--bump-version",
|
|
10905
11414
|
"Bump the version chain (use for a content reinterpretation, e.g. a type promotion)",
|
|
10906
11415
|
false
|
|
10907
|
-
).option("--source <source>", "Contributing lane stamp (attribution)"
|
|
11416
|
+
).option("--source <source>", "Contributing lane stamp (attribution; default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11417
|
+
const source = resolveSourceLane(opts.source);
|
|
10908
11418
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10909
11419
|
const json = cmd.optsWithGlobals().json;
|
|
10910
11420
|
const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
|
|
@@ -10936,7 +11446,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10936
11446
|
}
|
|
10937
11447
|
const body = {
|
|
10938
11448
|
memoryId,
|
|
10939
|
-
source
|
|
11449
|
+
source,
|
|
10940
11450
|
bumpVersion: Boolean(opts.bumpVersion)
|
|
10941
11451
|
};
|
|
10942
11452
|
if (opts.title !== void 0) body.title = opts.title;
|
|
@@ -10960,13 +11470,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10960
11470
|
json
|
|
10961
11471
|
);
|
|
10962
11472
|
});
|
|
10963
|
-
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp
|
|
11473
|
+
memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11474
|
+
const source = resolveSourceLane(opts.source);
|
|
10964
11475
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10965
11476
|
const data = await runApi("Archiving memory", async () => {
|
|
10966
11477
|
const client = await makeClient(cfg);
|
|
10967
11478
|
return client.POST("/memories/{memoryId}/archive", {
|
|
10968
11479
|
params: { path: { memoryId } },
|
|
10969
|
-
body: { source
|
|
11480
|
+
body: { source }
|
|
10970
11481
|
});
|
|
10971
11482
|
});
|
|
10972
11483
|
emitAction(
|
|
@@ -10977,13 +11488,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10977
11488
|
});
|
|
10978
11489
|
memory.command("restore <memoryId>").description(
|
|
10979
11490
|
"Restore an archived memory (POST /memories/{memoryId}/restore)"
|
|
10980
|
-
).option("--source <source>", "Source / lane stamp
|
|
11491
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11492
|
+
const source = resolveSourceLane(opts.source);
|
|
10981
11493
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10982
11494
|
const data = await runApi("Restoring memory", async () => {
|
|
10983
11495
|
const client = await makeClient(cfg);
|
|
10984
11496
|
return client.POST("/memories/{memoryId}/restore", {
|
|
10985
11497
|
params: { path: { memoryId } },
|
|
10986
|
-
body: { source
|
|
11498
|
+
body: { source }
|
|
10987
11499
|
});
|
|
10988
11500
|
});
|
|
10989
11501
|
emitAction(
|
|
@@ -10997,7 +11509,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10997
11509
|
).requiredOption(
|
|
10998
11510
|
"--owner-type <ownerType>",
|
|
10999
11511
|
"Unfiled | Workspace | Project | Candidate"
|
|
11000
|
-
).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp
|
|
11512
|
+
).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11513
|
+
const source = resolveSourceLane(opts.source);
|
|
11001
11514
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11002
11515
|
const data = await runApi("Moving memory", async () => {
|
|
11003
11516
|
const client = await makeClient(cfg);
|
|
@@ -11008,7 +11521,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11008
11521
|
type: opts.ownerType,
|
|
11009
11522
|
id: String(opts.ownerId ?? "")
|
|
11010
11523
|
},
|
|
11011
|
-
source
|
|
11524
|
+
source
|
|
11012
11525
|
}
|
|
11013
11526
|
});
|
|
11014
11527
|
});
|
|
@@ -11060,7 +11573,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11060
11573
|
).requiredOption(
|
|
11061
11574
|
"--content <content>",
|
|
11062
11575
|
"Reverted content JSON (the target version's content)"
|
|
11063
|
-
).option("--source <source>", "Source / lane stamp
|
|
11576
|
+
).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
|
|
11577
|
+
const source = resolveSourceLane(opts.source);
|
|
11064
11578
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11065
11579
|
const data = await runApi("Reverting memory", async () => {
|
|
11066
11580
|
const client = await makeClient(cfg);
|
|
@@ -11070,7 +11584,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11070
11584
|
fromVersion: Number(opts.fromVersion),
|
|
11071
11585
|
revertedContent: opts.content,
|
|
11072
11586
|
revertedText: opts.text,
|
|
11073
|
-
source
|
|
11587
|
+
source
|
|
11074
11588
|
}
|
|
11075
11589
|
});
|
|
11076
11590
|
});
|
|
@@ -11150,16 +11664,16 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
11150
11664
|
}
|
|
11151
11665
|
|
|
11152
11666
|
// src/setup/apply.ts
|
|
11153
|
-
import { createHash as createHash5, randomUUID as
|
|
11667
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
11154
11668
|
import {
|
|
11155
11669
|
chmodSync as chmodSync2,
|
|
11156
11670
|
copyFileSync,
|
|
11157
11671
|
existsSync as existsSync13,
|
|
11158
11672
|
mkdirSync as mkdirSync16,
|
|
11159
|
-
readFileSync as
|
|
11673
|
+
readFileSync as readFileSync17,
|
|
11160
11674
|
renameSync as renameSync4,
|
|
11161
|
-
rmSync as
|
|
11162
|
-
statSync as
|
|
11675
|
+
rmSync as rmSync8,
|
|
11676
|
+
statSync as statSync6,
|
|
11163
11677
|
writeFileSync as writeFileSync15
|
|
11164
11678
|
} from "fs";
|
|
11165
11679
|
import { dirname as dirname15 } from "path";
|
|
@@ -11226,7 +11740,7 @@ function ensureDir2(path) {
|
|
|
11226
11740
|
}
|
|
11227
11741
|
function readOr(path, fallback) {
|
|
11228
11742
|
try {
|
|
11229
|
-
return
|
|
11743
|
+
return readFileSync17(path, "utf8");
|
|
11230
11744
|
} catch {
|
|
11231
11745
|
return fallback;
|
|
11232
11746
|
}
|
|
@@ -11237,7 +11751,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
11237
11751
|
let current = {};
|
|
11238
11752
|
if (existed) {
|
|
11239
11753
|
try {
|
|
11240
|
-
current = JSON.parse(
|
|
11754
|
+
current = JSON.parse(readFileSync17(path, "utf8"));
|
|
11241
11755
|
} catch {
|
|
11242
11756
|
return {
|
|
11243
11757
|
kind: "mcp",
|
|
@@ -11607,24 +12121,24 @@ var defaultTomlFileOperations = {
|
|
|
11607
12121
|
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11608
12122
|
ensureDir2(path);
|
|
11609
12123
|
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11610
|
-
const temporary = `${path}.sechroom-${process.pid}-${
|
|
12124
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
11611
12125
|
const backup = `${path}.bak`;
|
|
11612
|
-
const backupTemporary = `${backup}.sechroom-${process.pid}-${
|
|
11613
|
-
const mode = existsSync13(path) ?
|
|
12126
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
12127
|
+
const mode = existsSync13(path) ? statSync6(path).mode & 4095 : 384;
|
|
11614
12128
|
try {
|
|
11615
12129
|
writeFileSync15(temporary, content, { mode });
|
|
11616
12130
|
chmodSync2(temporary, mode);
|
|
11617
|
-
validateToml(
|
|
12131
|
+
validateToml(readFileSync17(temporary, "utf8"));
|
|
11618
12132
|
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11619
12133
|
operations.copyFileSync(path, backupTemporary);
|
|
11620
12134
|
chmodSync2(backupTemporary, mode);
|
|
11621
|
-
validateToml(
|
|
12135
|
+
validateToml(readFileSync17(backupTemporary, "utf8"));
|
|
11622
12136
|
operations.renameSync(backupTemporary, backup);
|
|
11623
12137
|
}
|
|
11624
12138
|
operations.renameSync(temporary, path);
|
|
11625
12139
|
} finally {
|
|
11626
|
-
|
|
11627
|
-
|
|
12140
|
+
rmSync8(temporary, { force: true });
|
|
12141
|
+
rmSync8(backupTemporary, { force: true });
|
|
11628
12142
|
}
|
|
11629
12143
|
}
|
|
11630
12144
|
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
@@ -11913,7 +12427,7 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
11913
12427
|
|
|
11914
12428
|
// src/setup/skills-offer.ts
|
|
11915
12429
|
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync16 } from "fs";
|
|
11916
|
-
import { join as
|
|
12430
|
+
import { join as join20 } from "path";
|
|
11917
12431
|
|
|
11918
12432
|
// src/setup/lane-pin.ts
|
|
11919
12433
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -12029,8 +12543,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
12029
12543
|
if (skills.length > 0) {
|
|
12030
12544
|
const written = [];
|
|
12031
12545
|
for (const s of skills) {
|
|
12032
|
-
mkdirSync17(
|
|
12033
|
-
writeFileSync16(
|
|
12546
|
+
mkdirSync17(join20(sDir, s.name), { recursive: true });
|
|
12547
|
+
writeFileSync16(join20(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
12034
12548
|
written.push(s.name);
|
|
12035
12549
|
}
|
|
12036
12550
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -12042,7 +12556,7 @@ Found ${summary} available to you for ${surface}.
|
|
|
12042
12556
|
const written = [];
|
|
12043
12557
|
for (const a of agents) {
|
|
12044
12558
|
const file = `${a.name}.md`;
|
|
12045
|
-
writeFileSync16(
|
|
12559
|
+
writeFileSync16(join20(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
12046
12560
|
written.push(file);
|
|
12047
12561
|
}
|
|
12048
12562
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -12110,7 +12624,8 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
12110
12624
|
make = await promptYesNo("Make a personal copy to customise?");
|
|
12111
12625
|
}
|
|
12112
12626
|
if (make) {
|
|
12113
|
-
|
|
12627
|
+
const source = resolveSourceLane(void 0);
|
|
12628
|
+
await createOverride(cfg, resolved, personalWorkspaceId, source);
|
|
12114
12629
|
process.stderr.write(
|
|
12115
12630
|
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
12116
12631
|
`
|
|
@@ -12345,7 +12860,8 @@ Examples:
|
|
|
12345
12860
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12346
12861
|
const targets = clientTargets(process.cwd(), {
|
|
12347
12862
|
claudeDir: claudeTargets[0]?.dir,
|
|
12348
|
-
codexHome: codexHomes[0] ?? null
|
|
12863
|
+
codexHome: codexHomes[0] ?? null,
|
|
12864
|
+
codexScope: scope
|
|
12349
12865
|
});
|
|
12350
12866
|
const keys = resolveClientKeys(opts.client);
|
|
12351
12867
|
const json = g.json;
|
|
@@ -12473,7 +12989,10 @@ function registerSetup(program2, deps = {}) {
|
|
|
12473
12989
|
).option(
|
|
12474
12990
|
"--body <markdown>",
|
|
12475
12991
|
"section body (default: a TODO scaffold to edit later)"
|
|
12476
|
-
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
12992
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option(
|
|
12993
|
+
"--source <source>",
|
|
12994
|
+
"Source / lane stamp (default: this checkout's pinned code-lane)"
|
|
12995
|
+
).option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
12477
12996
|
"after",
|
|
12478
12997
|
`
|
|
12479
12998
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -12486,6 +13005,7 @@ Examples:
|
|
|
12486
13005
|
$ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
|
|
12487
13006
|
$ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
|
|
12488
13007
|
).action(async (titleParts, opts, cmd) => {
|
|
13008
|
+
const source = resolveSourceLane(opts.source);
|
|
12489
13009
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12490
13010
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
12491
13011
|
const title = titleParts.join(" ").trim();
|
|
@@ -12521,7 +13041,7 @@ Examples:
|
|
|
12521
13041
|
type: draft.kind,
|
|
12522
13042
|
content: "{}",
|
|
12523
13043
|
confidence: 1,
|
|
12524
|
-
source
|
|
13044
|
+
source,
|
|
12525
13045
|
archetype: "Document",
|
|
12526
13046
|
title: draft.title,
|
|
12527
13047
|
tags: draft.tags,
|
|
@@ -12637,15 +13157,30 @@ Examples:
|
|
|
12637
13157
|
"--client <list>",
|
|
12638
13158
|
`comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
12639
13159
|
DEFAULT_CLIENT_KEY
|
|
13160
|
+
).option(
|
|
13161
|
+
"--scope <scope>",
|
|
13162
|
+
"Codex MCP config scope: 'project' (<cwd>/.codex) or 'global' (CODEX_HOME / ~/.codex) \u2014 default project",
|
|
13163
|
+
"project"
|
|
12640
13164
|
).option("--dry-run", "print what would be written without writing", false).action(async (slug2, opts, cmd) => {
|
|
12641
13165
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12642
13166
|
const setup = await withSpinner(
|
|
12643
13167
|
"Fetching setup descriptors",
|
|
12644
13168
|
() => fetchSetup(cfg, slug2)
|
|
12645
13169
|
);
|
|
12646
|
-
|
|
13170
|
+
let scope;
|
|
13171
|
+
try {
|
|
13172
|
+
scope = resolveScope(opts.scope);
|
|
13173
|
+
} catch (error) {
|
|
13174
|
+
return fail(error.message);
|
|
13175
|
+
}
|
|
13176
|
+
const globals = cmd.optsWithGlobals();
|
|
13177
|
+
const codexHome = scope === "global" ? resolveCodexHomes({ override: globals.codexHome, scope })[0] ?? null : null;
|
|
13178
|
+
const targets = clientTargets(process.cwd(), {
|
|
13179
|
+
codexHome,
|
|
13180
|
+
codexScope: scope
|
|
13181
|
+
});
|
|
12647
13182
|
const keys = resolveClientKeys(opts.client);
|
|
12648
|
-
const json =
|
|
13183
|
+
const json = globals.json;
|
|
12649
13184
|
const result = [];
|
|
12650
13185
|
for (const key of keys) {
|
|
12651
13186
|
const target = targets[key];
|
|
@@ -12672,12 +13207,12 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
|
|
|
12672
13207
|
|
|
12673
13208
|
// src/commands/onboard.ts
|
|
12674
13209
|
import { existsSync as existsSync15 } from "fs";
|
|
12675
|
-
import { basename as basename5, join as
|
|
13210
|
+
import { basename as basename5, join as join22 } from "path";
|
|
12676
13211
|
|
|
12677
13212
|
// src/commands/fanout.ts
|
|
12678
13213
|
import { spawnSync } from "child_process";
|
|
12679
|
-
import { existsSync as existsSync14, readFileSync as
|
|
12680
|
-
import { isAbsolute as isAbsolute3, join as
|
|
13214
|
+
import { existsSync as existsSync14, readFileSync as readFileSync18, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
13215
|
+
import { isAbsolute as isAbsolute3, join as join21, resolve as resolve7 } from "path";
|
|
12681
13216
|
var ICON = {
|
|
12682
13217
|
refresh: "\u21BB",
|
|
12683
13218
|
bind: "+",
|
|
@@ -12690,20 +13225,20 @@ function resolveChildDir(path, root) {
|
|
|
12690
13225
|
function discoverChildren(root) {
|
|
12691
13226
|
let names;
|
|
12692
13227
|
try {
|
|
12693
|
-
names =
|
|
13228
|
+
names = readdirSync5(root);
|
|
12694
13229
|
} catch {
|
|
12695
13230
|
return [];
|
|
12696
13231
|
}
|
|
12697
13232
|
const out = [];
|
|
12698
13233
|
for (const name of names.sort()) {
|
|
12699
13234
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
12700
|
-
const dir =
|
|
13235
|
+
const dir = join21(root, name);
|
|
12701
13236
|
try {
|
|
12702
|
-
if (!
|
|
13237
|
+
if (!statSync7(dir).isDirectory()) continue;
|
|
12703
13238
|
} catch {
|
|
12704
13239
|
continue;
|
|
12705
13240
|
}
|
|
12706
|
-
if (existsSync14(
|
|
13241
|
+
if (existsSync14(join21(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
12707
13242
|
}
|
|
12708
13243
|
return out;
|
|
12709
13244
|
}
|
|
@@ -12711,7 +13246,7 @@ function readManifest(path) {
|
|
|
12711
13246
|
if (!existsSync14(path)) return null;
|
|
12712
13247
|
let parsed;
|
|
12713
13248
|
try {
|
|
12714
|
-
parsed = JSON.parse(
|
|
13249
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
12715
13250
|
} catch (err2) {
|
|
12716
13251
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
12717
13252
|
}
|
|
@@ -13084,11 +13619,20 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
13084
13619
|
clientId: persisted.clientId
|
|
13085
13620
|
};
|
|
13086
13621
|
}
|
|
13087
|
-
|
|
13088
|
-
if (process.env.SECHROOM_TOKEN) return;
|
|
13622
|
+
function hasUsableCredential() {
|
|
13623
|
+
if (process.env.SECHROOM_TOKEN) return true;
|
|
13089
13624
|
const cached = readToken();
|
|
13090
|
-
|
|
13091
|
-
|
|
13625
|
+
return Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
13626
|
+
}
|
|
13627
|
+
function requireCredentialForPreview(preview, dryRun) {
|
|
13628
|
+
if (!preview || hasUsableCredential()) return;
|
|
13629
|
+
const flag = dryRun ? "--dry-run" : "--check";
|
|
13630
|
+
fail(
|
|
13631
|
+
`sechroom onboard ${flag} needs an existing sign-in; run \`sechroom login\` first, then re-run the preview.`
|
|
13632
|
+
);
|
|
13633
|
+
}
|
|
13634
|
+
async function ensureAuth(cfg, yes) {
|
|
13635
|
+
if (hasUsableCredential()) return;
|
|
13092
13636
|
if (!canPrompt() || yes) {
|
|
13093
13637
|
fail(
|
|
13094
13638
|
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
@@ -13179,7 +13723,7 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
13179
13723
|
reason: "directory does not exist"
|
|
13180
13724
|
};
|
|
13181
13725
|
}
|
|
13182
|
-
if (existsSync15(
|
|
13726
|
+
if (existsSync15(join22(dir, ".sechroom.json"))) {
|
|
13183
13727
|
return {
|
|
13184
13728
|
label: entry.path,
|
|
13185
13729
|
dir,
|
|
@@ -13286,7 +13830,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
13286
13830
|
async function runRecurse(cfg, g, opts) {
|
|
13287
13831
|
const { yes, dryRun, json } = opts;
|
|
13288
13832
|
const root = process.cwd();
|
|
13289
|
-
const manifestPath =
|
|
13833
|
+
const manifestPath = join22(root, ".sechroom", "repos.json");
|
|
13290
13834
|
const fromManifest = readManifest(manifestPath);
|
|
13291
13835
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
13292
13836
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -13427,6 +13971,7 @@ Examples:
|
|
|
13427
13971
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
13428
13972
|
if (opts.recurse) {
|
|
13429
13973
|
const baseUrl2 = resolveBaseUrl(g);
|
|
13974
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13430
13975
|
await ensureAuth(
|
|
13431
13976
|
{
|
|
13432
13977
|
baseUrl: baseUrl2,
|
|
@@ -13451,6 +13996,7 @@ Examples:
|
|
|
13451
13996
|
return;
|
|
13452
13997
|
}
|
|
13453
13998
|
const baseUrl = resolveBaseUrl(g);
|
|
13999
|
+
requireCredentialForPreview(dryRun || check, dryRun);
|
|
13454
14000
|
await ensureAuth(
|
|
13455
14001
|
{
|
|
13456
14002
|
baseUrl,
|
|
@@ -13467,7 +14013,12 @@ Examples:
|
|
|
13467
14013
|
local: Boolean(opts.local) || scope === "project",
|
|
13468
14014
|
here: scope === "project" ? true : Boolean(opts.here),
|
|
13469
14015
|
workspace: opts.workspace,
|
|
13470
|
-
|
|
14016
|
+
// Both previews are read-only. `--dry-run` promises to "walk through without
|
|
14017
|
+
// writing files or changing the profile", but only `--check` was in this
|
|
14018
|
+
// guard, so a dry run could still create or rewrite `.sechroom.json` /
|
|
14019
|
+
// `~/.config/sechroom/config.json` before the operator approved anything
|
|
14020
|
+
// (FR-sechroom-710). Matches how `ensureTimezone` is already gated below.
|
|
14021
|
+
persist: !(dryRun || check)
|
|
13471
14022
|
});
|
|
13472
14023
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
13473
14024
|
if (!json && tz.action !== "already-set") {
|
|
@@ -13535,20 +14086,15 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
13535
14086
|
yes,
|
|
13536
14087
|
process.cwd()
|
|
13537
14088
|
);
|
|
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
|
-
}
|
|
14089
|
+
const keys = requestedKeys;
|
|
13545
14090
|
const setup = await withSpinner(
|
|
13546
14091
|
"Fetching setup descriptors",
|
|
13547
14092
|
() => fetchSetup(cfg)
|
|
13548
14093
|
);
|
|
13549
14094
|
const targets = clientTargets(process.cwd(), {
|
|
13550
14095
|
claudeDir: claudeTargets[0]?.dir,
|
|
13551
|
-
codexHome: codexHomes[0] ?? null
|
|
14096
|
+
codexHome: codexHomes[0] ?? null,
|
|
14097
|
+
codexScope: scope
|
|
13552
14098
|
});
|
|
13553
14099
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
13554
14100
|
if (!dryRun && !check) {
|
|
@@ -14078,31 +14624,31 @@ Examples:
|
|
|
14078
14624
|
|
|
14079
14625
|
// src/commands/reset.ts
|
|
14080
14626
|
import { homedir as homedir6 } from "os";
|
|
14081
|
-
import { join as
|
|
14082
|
-
import { existsSync as existsSync16, readFileSync as
|
|
14627
|
+
import { join as join23 } from "path";
|
|
14628
|
+
import { existsSync as existsSync16, readFileSync as readFileSync19, rmSync as rmSync9 } from "fs";
|
|
14083
14629
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
14084
|
-
var localSkillsDir = () =>
|
|
14085
|
-
var globalSkillsDir = () =>
|
|
14086
|
-
var localAgentsDir = () =>
|
|
14087
|
-
var globalAgentsDir = () =>
|
|
14630
|
+
var localSkillsDir = () => join23(process.cwd(), ".claude", "skills");
|
|
14631
|
+
var globalSkillsDir = () => join23(homedir6(), ".claude", "skills");
|
|
14632
|
+
var localAgentsDir = () => join23(process.cwd(), ".claude", "agents");
|
|
14633
|
+
var globalAgentsDir = () => join23(homedir6(), ".claude", "agents");
|
|
14088
14634
|
function removeMaterialisedSkills(dir) {
|
|
14089
14635
|
const removed = [];
|
|
14090
|
-
const lockPath =
|
|
14636
|
+
const lockPath = join23(dir, SKILLS_LOCK2);
|
|
14091
14637
|
if (!existsSync16(lockPath)) return removed;
|
|
14092
14638
|
try {
|
|
14093
|
-
const lock = JSON.parse(
|
|
14639
|
+
const lock = JSON.parse(readFileSync19(lockPath, "utf8"));
|
|
14094
14640
|
for (const entry of Object.values(lock)) {
|
|
14095
14641
|
for (const name of entry.skills ?? []) {
|
|
14096
|
-
const p =
|
|
14642
|
+
const p = join23(dir, name);
|
|
14097
14643
|
if (existsSync16(p)) {
|
|
14098
|
-
|
|
14644
|
+
rmSync9(p, { recursive: true, force: true });
|
|
14099
14645
|
removed.push(p);
|
|
14100
14646
|
}
|
|
14101
14647
|
}
|
|
14102
14648
|
}
|
|
14103
14649
|
} catch {
|
|
14104
14650
|
}
|
|
14105
|
-
|
|
14651
|
+
rmSync9(lockPath, { force: true });
|
|
14106
14652
|
removed.push(lockPath);
|
|
14107
14653
|
return removed;
|
|
14108
14654
|
}
|
|
@@ -14139,19 +14685,19 @@ function registerReset(program2) {
|
|
|
14139
14685
|
}
|
|
14140
14686
|
}
|
|
14141
14687
|
const removed = [];
|
|
14142
|
-
const stateDir =
|
|
14688
|
+
const stateDir = join23(process.cwd(), ".sechroom");
|
|
14143
14689
|
if (existsSync16(stateDir)) {
|
|
14144
|
-
|
|
14690
|
+
rmSync9(stateDir, { recursive: true, force: true });
|
|
14145
14691
|
removed.push(stateDir);
|
|
14146
14692
|
}
|
|
14147
|
-
const legacyCfg =
|
|
14693
|
+
const legacyCfg = join23(process.cwd(), ".sechroom.json");
|
|
14148
14694
|
if (existsSync16(legacyCfg)) {
|
|
14149
|
-
|
|
14695
|
+
rmSync9(legacyCfg, { force: true });
|
|
14150
14696
|
removed.push(legacyCfg);
|
|
14151
14697
|
}
|
|
14152
|
-
const legacySem =
|
|
14698
|
+
const legacySem = join23(process.cwd(), ".sem");
|
|
14153
14699
|
if (existsSync16(legacySem)) {
|
|
14154
|
-
|
|
14700
|
+
rmSync9(legacySem, { force: true });
|
|
14155
14701
|
removed.push(legacySem);
|
|
14156
14702
|
}
|
|
14157
14703
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -14176,8 +14722,8 @@ function registerReset(program2) {
|
|
|
14176
14722
|
}
|
|
14177
14723
|
|
|
14178
14724
|
// src/commands/skills.ts
|
|
14179
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
14180
|
-
import { join as
|
|
14725
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
|
|
14726
|
+
import { join as join24 } from "path";
|
|
14181
14727
|
function filenameFromDisposition(header) {
|
|
14182
14728
|
if (!header) return void 0;
|
|
14183
14729
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -14185,11 +14731,11 @@ function filenameFromDisposition(header) {
|
|
|
14185
14731
|
}
|
|
14186
14732
|
function resolveOutputPath(output, serverFilename) {
|
|
14187
14733
|
const filename = serverFilename || "skills.zip";
|
|
14188
|
-
if (!output) return
|
|
14189
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
14734
|
+
if (!output) return join24(process.cwd(), filename);
|
|
14735
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync8(output).isDirectory();
|
|
14190
14736
|
if (looksLikeDir) {
|
|
14191
14737
|
mkdirSync18(output, { recursive: true });
|
|
14192
|
-
return
|
|
14738
|
+
return join24(output, filename);
|
|
14193
14739
|
}
|
|
14194
14740
|
return output;
|
|
14195
14741
|
}
|
|
@@ -14235,6 +14781,7 @@ Examples:
|
|
|
14235
14781
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
14236
14782
|
$ sechroom skills list what's materialised on disk
|
|
14237
14783
|
$ sechroom skills clean remove the materialised skill files
|
|
14784
|
+
$ sechroom skills clean --prune-orphans also sweep skills orphaned by an earlier rename
|
|
14238
14785
|
$ sechroom skills preview --workspace wsp_abc render a draft bundle from source (no install)
|
|
14239
14786
|
$ sechroom skills package my-bundle download the installed bundle's skills as a zip
|
|
14240
14787
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
@@ -14248,7 +14795,7 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14248
14795
|
);
|
|
14249
14796
|
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
14797
|
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));
|
|
14798
|
+
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
14799
|
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
14800
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
14254
14801
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -14395,8 +14942,8 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14395
14942
|
|
|
14396
14943
|
// src/commands/sweep.ts
|
|
14397
14944
|
import { existsSync as existsSync18 } from "fs";
|
|
14398
|
-
import { dirname as dirname16, join as
|
|
14399
|
-
var DEFAULT_MANIFEST =
|
|
14945
|
+
import { dirname as dirname16, join as join25, resolve as resolve8 } from "path";
|
|
14946
|
+
var DEFAULT_MANIFEST = join25(".sechroom", "repos.json");
|
|
14400
14947
|
function planEntry(entry, root) {
|
|
14401
14948
|
const dir = resolveChildDir(entry.path, root);
|
|
14402
14949
|
if (!existsSync18(dir)) {
|
|
@@ -14502,14 +15049,15 @@ Examples:
|
|
|
14502
15049
|
$ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
|
|
14503
15050
|
$ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
|
|
14504
15051
|
);
|
|
14505
|
-
worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId
|
|
15052
|
+
worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId (default: this checkout's pinned code-lane)").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
|
|
15053
|
+
const source = resolveSourceLane(opts.source);
|
|
14506
15054
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
14507
15055
|
const data = await runApi("Appending work-log entry", async () => {
|
|
14508
15056
|
const client = await makeClient(cfg);
|
|
14509
15057
|
return client.POST("/operator-surface/work-log/append", {
|
|
14510
15058
|
body: {
|
|
14511
15059
|
bullet: opts.text,
|
|
14512
|
-
laneId:
|
|
15060
|
+
laneId: source,
|
|
14513
15061
|
workspaceId: opts.workspace ?? null,
|
|
14514
15062
|
title: opts.title ?? null
|
|
14515
15063
|
}
|
|
@@ -14900,7 +15448,7 @@ async function readStdin4() {
|
|
|
14900
15448
|
function resolveVersion() {
|
|
14901
15449
|
try {
|
|
14902
15450
|
const pkg = JSON.parse(
|
|
14903
|
-
|
|
15451
|
+
readFileSync20(new URL("../package.json", import.meta.url), "utf8")
|
|
14904
15452
|
);
|
|
14905
15453
|
return pkg.version ?? "0.0.0";
|
|
14906
15454
|
} catch {
|