@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.
Files changed (2) hide show
  1. package/dist/index.js +960 -412
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync19 } from "fs";
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 problemDetail = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
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
- ...problemDetail ? [problemDetail] : [],
710
+ ...problemDetail2 ? [problemDetail2] : [],
711
711
  ...fieldErrors,
712
712
  ...structuredErrors
713
713
  ];
714
714
  if (parts.length > 0) {
715
- if (title && !problemDetail && fieldErrors.length === 0 && structuredErrors.length === 0) {
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 rmSync2, writeFileSync as writeFileSync3 } from "fs";
1005
- import { join as join4 } from "path";
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: "cli-agent-instructions-customize",
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(join4(dir, s.name), { recursive: true });
1383
- writeFileSync3(join4(dir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
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 = `${a.name}.${codex ? "toml" : "md"}`;
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(join4(dir, file), body);
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 = join4(dir, s.name, "references");
1535
+ const refDir = join5(dir, s.name, "references");
1450
1536
  mkdirSync3(refDir, { recursive: true });
1451
1537
  for (const r of refs) {
1452
- writeFileSync3(join4(refDir, `${r.name}.md`), r.body.endsWith("\n") ? r.body : r.body + "\n");
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 written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
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(join4(t.dir, name)) }))
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(join4(t.dir, SKILLS_LOCK));
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.skills) {
1647
- const p = join4(t.dir, name);
1760
+ for (const name of entry?.skills ?? []) {
1761
+ const p = join5(t.dir, name);
1648
1762
  if (existsSync3(p)) {
1649
- rmSync2(p, { recursive: true, force: true });
1763
+ rmSync3(p, { recursive: true, force: true });
1650
1764
  removed.push(name);
1651
1765
  }
1652
1766
  }
1653
- delete lock[slug2];
1654
- writeSkillsLock(t.dir, lock);
1655
- cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
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 readFileSync8, writeFileSync as writeFileSync9 } from "fs";
1691
- import { dirname as dirname9, join as join12 } from "path";
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 { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
2061
- import { dirname as dirname8, join as join11 } from "path";
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 join5 } from "path";
2065
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
2066
- var SEM_FILE = join5(".sechroom", "lane.json");
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 join5(cwd, SEM_FILE);
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 = join5(dir, SEM_FILE);
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 = join5(dir, ".git");
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 || statSync(gitPath).isDirectory()) return lane;
2096
- const gitFile = readFileSync3(gitPath, "utf8");
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 = join5(common[1], "worktrees");
2100
- const siblings = readdirSync(worktreesDir).filter((n) => {
2286
+ const worktreesDir = join6(common[1], "worktrees");
2287
+ const siblings = readdirSync2(worktreesDir).filter((n) => {
2101
2288
  try {
2102
- return statSync(join5(worktreesDir, n)).isDirectory();
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(readFileSync3(p, "utf8")) };
2311
+ return { path: p, values: parseLaneJson(readFileSync4(p, "utf8")) };
2125
2312
  }
2126
2313
  function readLocalSemValues(cwd = process.cwd()) {
2127
- const next = join5(cwd, SEM_FILE);
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 = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
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(join5(dir, ".git"))) return true;
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 = join5(dir, ".gitignore");
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(join5(dir, ".git")) || parent === dir) {
2201
- return { path: join5(startDir, ".gitignore"), exists: false };
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 = readFileSync3(target.path, "utf8");
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 join10, resolve as resolve3 } from "path";
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 readFileSync4,
3370
- rmSync as rmSync3,
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 join6, resolve } from "path";
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 = readFileSync4(path, "utf8");
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 = readFileSync4(path, "utf8");
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
- rmSync3(path, { force: true });
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
- rmSync3(path, { force: true });
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 join6(tmpdir(), `sechroom-fleet-${hash}.sock`);
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
- rmSync3(socketPath, { force: true });
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
- rmSync3(socketPath, { force: true });
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 readFileSync6,
4475
- rmSync as rmSync4,
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 join9, parse, resolve as resolve2, sep } from "path";
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 readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4483
- import { delimiter, dirname as dirname6, join as join8 } from "path";
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 join7 } from "path";
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 join7(
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 join7(
4501
- process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
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 join7(home, ".config", "Claude", "claude_desktop_config.json");
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 ?? join7(home, ".claude");
4512
- const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
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: join7(cwd, ".mcp.json"),
4769
+ path: join8(cwd, ".mcp.json"),
4521
4770
  format: "json"
4522
4771
  },
4523
- instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
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: join7(claudeDir, "CLAUDE.md")
4785
+ path: join8(claudeDir, "CLAUDE.md")
4537
4786
  }
4538
4787
  },
4539
4788
  codex: {
4540
4789
  key: "codex",
4541
4790
  label: "Codex CLI",
4542
- mcp: codexHome ? {
4791
+ mcp: codexConfigPath ? {
4543
4792
  surfaceKey: "chatgpt",
4544
4793
  sectionType: SectionType.McpConfigToml,
4545
- path: join7(codexHome, "config.toml"),
4794
+ path: codexConfigPath,
4546
4795
  format: "toml"
4547
4796
  } : null,
4548
- instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
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: join7(cwd, ".cursor", "mcp.json"),
4805
+ path: join8(cwd, ".cursor", "mcp.json"),
4557
4806
  format: "json"
4558
4807
  },
4559
- instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
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: join7(home, ".gemini", "config", "mcp_config.json"),
4822
+ path: join8(home, ".gemini", "config", "mcp_config.json"),
4574
4823
  format: "json"
4575
4824
  },
4576
- instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
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(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
4845
+ if (existsSync5(join8(home, ".cursor")) || existsSync5(join8(cwd, ".cursor")))
4597
4846
  detected.push("cursor");
4598
- if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
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 = readFileSync5(path, "utf8");
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) && readFileSync5(path, "utf8").trim().length > 0;
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(join8(claudeDir, "settings.json"), commands, dryRun);
4907
+ return installHooksJson(join9(claudeDir, "settings.json"), commands, dryRun);
4659
4908
  }
4660
4909
  function installCodexCommands(codexHome, commands, dryRun) {
4661
4910
  return [
4662
- installHooksJson(join8(codexHome, "hooks.json"), commands, dryRun),
4663
- installCodexFeatureFlag(join8(codexHome, "config.toml"), dryRun)
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 ? readFileSync5(path, "utf8") : "";
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 = join8(opts.claudeDir, "settings.json");
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(join8(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
4722
- const featureFlag = installCodexFeatureFlag(join8(opts.codexHome, "config.toml"), opts.dryRun);
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(join8(dir, name))) return true;
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").action((opts, cmd) => {
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 = join9(process.cwd(), ".sechroom");
5107
+ const dir = join10(process.cwd(), ".sechroom");
4843
5108
  mkdirSync8(dir, { recursive: true });
4844
- const path = join9(dir, BINDING_FILE);
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 = join9(process.cwd(), ".sechroom", BINDING_FILE);
5131
+ const path = join10(process.cwd(), ".sechroom", BINDING_FILE);
4865
5132
  const existed = existsSync7(path);
4866
- if (existed) rmSync4(path);
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 = join9(dir, ".sechroom", BINDING_FILE);
5255
+ const path = join10(dir, ".sechroom", BINDING_FILE);
4987
5256
  if (existsSync7(path)) {
4988
5257
  try {
4989
5258
  const b = JSON.parse(
4990
- readFileSync6(path, "utf8")
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
- readFileSync6(binding.path, "utf8")
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
- readFileSync6(binding.path, "utf8")
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
- readFileSync6(binding.path, "utf8")
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 readFileSync6(path, "utf8").split("\n")) {
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 { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed), modelId: model || null };
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(base("Terminal", { text: input.last_assistant_message ?? null }));
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}`) ? join9(homedir4(), trimmed.slice(2)) : trimmed;
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) : join10(process.cwd(), ".sechroom", "fleet.pid")
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) : join10(process.cwd(), ".sechroom", "fleet.log")
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 = join10(
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) : join10(process.cwd(), ".sechroom", "fleet.pid")
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 = join11(checkout, ".sechroom", EXECUTOR_STATE);
6082
- const previous = existsSync8(statePath) ? JSON.parse(readFileSync7(statePath, "utf8")) : void 0;
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
- ) : [join11(checkout, ".claude")];
6112
- const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
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 = join11(
6648
- sem ? dirname8(sem.path) : join11(start, ".sechroom"),
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(readFileSync7(path, "utf8")),
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
- try {
6756
- await holdOpen(conn);
6757
- } finally {
6758
- stopReconciliation();
6759
- stopHeartbeat();
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
- try {
6813
- await holdOpen(conn);
6814
- } finally {
6815
- stopReconciliation();
6816
- stopHeartbeat();
6817
- leaseHeartbeats.stop();
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 = join12(process.cwd(), ".mcp.json");
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 ?? api;
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
- return startLeaseHeartbeat(
6930
- () => request(
6931
- cfg,
6932
- `/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
6933
- {
6934
- method: "POST",
6935
- body: JSON.stringify({
6936
- claimToken,
6937
- tokenVersion: claim.tokenVersion ?? 1
6938
- })
6939
- }
6940
- ),
6941
- onError,
6942
- intervalMilliseconds,
6943
- {
6944
- setInterval: dependencies.setInterval,
6945
- clearInterval: dependencies.clearInterval
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 = readFileSync8(path, "utf8");
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 readFileSync11, statSync as statSync3, writeFileSync as writeFileSync13 } from "fs";
7206
- import { dirname as dirname13, join as join15 } from "path";
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 readFileSync9,
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 = readFileSync9(messagePath, "utf8").replace(/\r\n/g, "\n");
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 = readFileSync9(path, "utf8");
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) ? readFileSync9(path, "utf8") : "";
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 randomUUID2 } from "crypto";
7351
- import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
7352
- import { dirname as dirname12, join as join14 } from "path";
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 readdirSync2,
7360
- readFileSync as readFileSync10,
7696
+ readdirSync as readdirSync3,
7697
+ readFileSync as readFileSync11,
7361
7698
  renameSync as renameSync2,
7362
7699
  rmdirSync,
7363
- rmSync as rmSync5,
7364
- statSync as statSync2,
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 join13, relative, resolve as resolve5 } from "path";
7368
- var COMPILED_SKILL_LOCK = join13(
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 = join13(cwd, COMPILED_SKILL_LOCK);
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) ? readFileSync10(path, "utf8") : void 0;
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(readFileSync10(path, "utf8")) !== existingHash)) {
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 join13(skillsRoot, name, "SKILL.md");
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(readFileSync10(path, "utf8"));
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}-${randomUUID()}`;
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(readFileSync10(quarantine, "utf8"));
7965
+ const quarantinedHash = sha256(readFileSync11(quarantine, "utf8"));
7629
7966
  if (acceptableHashes.includes(quarantinedHash)) {
7630
- rmSync5(quarantine);
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-${randomUUID()}`);
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 = readFileSync10(path, "utf8");
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 = join13(cwd, ".git");
7997
+ const dotGit = join14(cwd, ".git");
7661
7998
  try {
7662
- if (statSync2(dotGit).isDirectory()) return join13(dotGit, "info", "exclude");
7663
- const pointer = readFileSync10(dotGit, "utf8").trim();
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 join13(gitDir, "info", "exclude");
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 (readdirSync2(path).length === 0) rmdirSync(path);
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 = join14(".sechroom", "CLAUDE.md");
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 = join14(root, DYNAMIC_AGENT_CONTEXT_FILE);
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}.${randomUUID2()}.tmp`;
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
- rmSync6(temporaryPath, { force: true });
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
- var INTENT_FILE = join15(".sechroom", "continuity.json");
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 = join15(dir, INTENT_FILE);
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(readFileSync11(path, "utf8"));
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) : join15(start, ".sechroom");
7871
- return join15(dir, ".checkpoint-state.json");
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(readFileSync11(p, "utf8"));
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 (statSync3(path).mtimeMs <= ledger.lastMtimeMs) return true;
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 = statSync3(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 path = resolveIntentPath(cwd) ?? join16(cwd, INTENT_FILE);
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 readFileSync12 } from "fs";
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", "cli").addHelpText(
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 ? readFileSync12(opts.file, "utf8") : readFileSync12(0, "utf8");
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: opts.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: opts.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 readFileSync13 } from "fs";
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 join17 } from "path";
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 join17(home, DEFAULT_HERDR_SOCKET_RELATIVE);
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 = () => readFileSync13(0, "utf8")) {
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 readFileSync15 } from "fs";
10809
+ import { readFileSync as readFileSync16 } from "fs";
10305
10810
 
10306
10811
  // src/commands/memory-import.ts
10307
- import { readdirSync as readdirSync3, readFileSync as readFileSync14, realpathSync, statSync as statSync4 } from "fs";
10308
- import { basename as basename4, join as join18, resolve as resolve6 } from "path";
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 = readdirSync3(dir, { withFileTypes: true }).sort(
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 = join18(dir, entry.name);
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 = statSync4(child);
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 = statSync4(input).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 = readFileSync14(path, "utf8");
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 ? readFileSync15(0, "utf8") : readFileSync15(String(fileOpt), "utf8");
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", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
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: opts.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", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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)", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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", "cli").action(async (memoryId, opts, cmd) => {
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: opts.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 randomUUID3 } from "crypto";
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 readFileSync16,
11673
+ readFileSync as readFileSync17,
11160
11674
  renameSync as renameSync4,
11161
- rmSync as rmSync7,
11162
- statSync as statSync5,
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 readFileSync16(path, "utf8");
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(readFileSync16(path, "utf8"));
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}-${randomUUID3()}.tmp`;
12124
+ const temporary = `${path}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
11611
12125
  const backup = `${path}.bak`;
11612
- const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11613
- const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
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(readFileSync16(temporary, "utf8"));
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(readFileSync16(backupTemporary, "utf8"));
12135
+ validateToml(readFileSync17(backupTemporary, "utf8"));
11622
12136
  operations.renameSync(backupTemporary, backup);
11623
12137
  }
11624
12138
  operations.renameSync(temporary, path);
11625
12139
  } finally {
11626
- rmSync7(temporary, { force: true });
11627
- rmSync7(backupTemporary, { force: true });
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 join19 } from "path";
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(join19(sDir, s.name), { recursive: true });
12033
- writeFileSync16(join19(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
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(join19(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
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
- await createOverride(cfg, resolved, personalWorkspaceId);
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("--dry-run", "print what would be authored; write nothing", false).addHelpText(
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: "cli-new-convention",
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
- const targets = clientTargets(process.cwd());
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 = cmd.optsWithGlobals().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 join21 } from "path";
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 readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
12680
- import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
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 = readdirSync4(root);
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 = join20(root, name);
13235
+ const dir = join21(root, name);
12701
13236
  try {
12702
- if (!statSync6(dir).isDirectory()) continue;
13237
+ if (!statSync7(dir).isDirectory()) continue;
12703
13238
  } catch {
12704
13239
  continue;
12705
13240
  }
12706
- if (existsSync14(join20(dir, ".git")) || committedBindingPath(dir)) out.push(name);
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(readFileSync17(path, "utf8"));
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
- async function ensureAuth(cfg, yes) {
13088
- if (process.env.SECHROOM_TOKEN) return;
13622
+ function hasUsableCredential() {
13623
+ if (process.env.SECHROOM_TOKEN) return true;
13089
13624
  const cached = readToken();
13090
- const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
13091
- if (usable) return;
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(join21(dir, ".sechroom.json"))) {
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 = join21(root, ".sechroom", "repos.json");
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
- persist: !check
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 = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
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 join22 } from "path";
14082
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
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 = () => join22(process.cwd(), ".claude", "skills");
14085
- var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
14086
- var localAgentsDir = () => join22(process.cwd(), ".claude", "agents");
14087
- var globalAgentsDir = () => join22(homedir6(), ".claude", "agents");
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 = join22(dir, SKILLS_LOCK2);
14636
+ const lockPath = join23(dir, SKILLS_LOCK2);
14091
14637
  if (!existsSync16(lockPath)) return removed;
14092
14638
  try {
14093
- const lock = JSON.parse(readFileSync18(lockPath, "utf8"));
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 = join22(dir, name);
14642
+ const p = join23(dir, name);
14097
14643
  if (existsSync16(p)) {
14098
- rmSync8(p, { recursive: true, force: true });
14644
+ rmSync9(p, { recursive: true, force: true });
14099
14645
  removed.push(p);
14100
14646
  }
14101
14647
  }
14102
14648
  }
14103
14649
  } catch {
14104
14650
  }
14105
- rmSync8(lockPath, { force: true });
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 = join22(process.cwd(), ".sechroom");
14688
+ const stateDir = join23(process.cwd(), ".sechroom");
14143
14689
  if (existsSync16(stateDir)) {
14144
- rmSync8(stateDir, { recursive: true, force: true });
14690
+ rmSync9(stateDir, { recursive: true, force: true });
14145
14691
  removed.push(stateDir);
14146
14692
  }
14147
- const legacyCfg = join22(process.cwd(), ".sechroom.json");
14693
+ const legacyCfg = join23(process.cwd(), ".sechroom.json");
14148
14694
  if (existsSync16(legacyCfg)) {
14149
- rmSync8(legacyCfg, { force: true });
14695
+ rmSync9(legacyCfg, { force: true });
14150
14696
  removed.push(legacyCfg);
14151
14697
  }
14152
- const legacySem = join22(process.cwd(), ".sem");
14698
+ const legacySem = join23(process.cwd(), ".sem");
14153
14699
  if (existsSync16(legacySem)) {
14154
- rmSync8(legacySem, { force: true });
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 statSync7, writeFileSync as writeFileSync17 } from "fs";
14180
- import { join as join23 } from "path";
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 join23(process.cwd(), filename);
14189
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
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 join23(output, filename);
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 join24, resolve as resolve8 } from "path";
14399
- var DEFAULT_MANIFEST = join24(".sechroom", "repos.json");
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", "cli").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
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: opts.source ?? null,
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
- readFileSync19(new URL("../package.json", import.meta.url), "utf8")
15451
+ readFileSync20(new URL("../package.json", import.meta.url), "utf8")
14904
15452
  );
14905
15453
  return pkg.version ?? "0.0.0";
14906
15454
  } catch {