@sechroom/cli 2026.8.8-rc.56311f4a3 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +917 -336
  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
@@ -642,6 +642,17 @@ function emitAction(summary, data, json) {
642
642
  process.stdout.write(`${ok("\u2713")} ${summary}
643
643
  `);
644
644
  }
645
+ var JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
646
+ function confidenceWireValue(value) {
647
+ if (value === null || value === void 0 || value === "") return null;
648
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
649
+ const raw = String(value).trim();
650
+ if (JSON_NUMBER.test(raw)) {
651
+ const numeric = Number(raw);
652
+ if (Number.isFinite(numeric)) return numeric;
653
+ }
654
+ return String(value);
655
+ }
645
656
  var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
646
657
  function isGovernanceQueued(body) {
647
658
  return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
@@ -688,23 +699,57 @@ function formatFailureMessage(error) {
688
699
  if (error.cause instanceof Error && error.cause.message) {
689
700
  msg += `: ${error.cause.message}`;
690
701
  }
691
- } else if (typeof error === "object" && error !== null && "title" in error) {
702
+ } else if (isRecord(error)) {
692
703
  const problem = error;
693
- msg = String(problem.title);
694
- if (problem.errors && typeof problem.errors === "object") {
695
- const detail = Object.entries(problem.errors).flatMap(
696
- ([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
697
- );
698
- if (detail.length > 0) msg += `
699
- ${detail.join("\n")}`;
704
+ const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
705
+ const problemDetail2 = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
706
+ const fieldErrors = formatProblemErrors(problem.errors);
707
+ const structuredErrors = fieldErrors.length === 0 ? formatStructuredViolations(problem.violations) : [];
708
+ const parts = [
709
+ ...title ? [title] : [],
710
+ ...problemDetail2 ? [problemDetail2] : [],
711
+ ...fieldErrors,
712
+ ...structuredErrors
713
+ ];
714
+ if (parts.length > 0) {
715
+ if (title && !problemDetail2 && fieldErrors.length === 0 && structuredErrors.length === 0) {
716
+ parts.push("No additional error detail was returned by the API.");
717
+ }
718
+ msg = parts.join("\n");
719
+ } else {
720
+ const serialized = JSON.stringify(error);
721
+ msg = serialized && serialized !== "{}" ? serialized : "The API returned an empty error response.";
700
722
  }
701
- } else if (typeof error === "object" && error !== null) {
702
- msg = JSON.stringify(error);
703
723
  } else {
704
724
  msg = String(error);
705
725
  }
706
726
  return msg;
707
727
  }
728
+ function isRecord(value) {
729
+ return typeof value === "object" && value !== null && !Array.isArray(value);
730
+ }
731
+ function formatProblemErrors(errors) {
732
+ if (!isRecord(errors)) return [];
733
+ return Object.entries(errors).flatMap(
734
+ ([field, messages]) => (Array.isArray(messages) ? messages : [messages]).map(
735
+ (message) => ` ${field}: ${formatErrorValue(message)}`
736
+ )
737
+ );
738
+ }
739
+ function formatStructuredViolations(violations) {
740
+ if (!Array.isArray(violations)) return [];
741
+ return violations.flatMap((violation) => {
742
+ if (!isRecord(violation)) return [formatErrorValue(violation)];
743
+ const field = typeof violation.field === "string" ? violation.field : "validation";
744
+ const message = typeof violation.message === "string" ? violation.message : formatErrorValue(violation);
745
+ return [` ${field}: ${message}`];
746
+ });
747
+ }
748
+ function formatErrorValue(value) {
749
+ if (typeof value === "string") return value;
750
+ const serialized = JSON.stringify(value);
751
+ return serialized ?? String(value);
752
+ }
708
753
  function fail(error) {
709
754
  const msg = formatFailureMessage(error);
710
755
  process.stderr.write(`error: ${msg}
@@ -956,8 +1001,8 @@ function recordMaterialisedSkills(dir, slug2, skills, meta = {}) {
956
1001
  }
957
1002
 
958
1003
  // src/setup/materialise.ts
959
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
960
- 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";
961
1006
 
962
1007
  // src/setup/config-dirs.ts
963
1008
  import { homedir as homedir2 } from "os";
@@ -1027,14 +1072,19 @@ async function fetchSetup(cfg, namespaceSlug) {
1027
1072
  "/operator-surface/setup",
1028
1073
  hasQuery ? { params: { query } } : {}
1029
1074
  );
1030
- if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
1075
+ if (error)
1076
+ throw new Error(
1077
+ `GET /operator-surface/setup failed: ${JSON.stringify(error)}`
1078
+ );
1031
1079
  return data;
1032
1080
  }
1033
1081
  async function listNamespaces(cfg) {
1034
1082
  const client = await makeClient(cfg);
1035
1083
  const { data } = await client.GET("/mcp-aggregator/namespaces", {});
1036
1084
  const rows = data ?? [];
1037
- return rows.filter((r) => typeof r.slug === "string").map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
1085
+ return rows.filter(
1086
+ (r) => typeof r.slug === "string"
1087
+ ).map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
1038
1088
  }
1039
1089
  function findSurface(setup, surfaceKey) {
1040
1090
  return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
@@ -1050,10 +1100,15 @@ function sectionSnippet(section) {
1050
1100
  }
1051
1101
  return null;
1052
1102
  }
1053
- function parseTagArtifactId(id) {
1103
+ function parseTagArtifactCandidates(id) {
1054
1104
  if (!id.startsWith("tag:")) return null;
1055
- const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1056
- return tags.length > 0 ? tags : null;
1105
+ const candidates = id.slice("tag:".length).split("|").map(
1106
+ (set) => set.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
1107
+ ).filter((set) => set.length > 0);
1108
+ return candidates.length > 0 ? candidates : null;
1109
+ }
1110
+ function parseTagArtifactId(id) {
1111
+ return parseTagArtifactCandidates(id)?.[0] ?? null;
1057
1112
  }
1058
1113
  async function getPersonalWorkspaceId(cfg) {
1059
1114
  const client = await makeClient(cfg);
@@ -1062,40 +1117,102 @@ async function getPersonalWorkspaceId(cfg) {
1062
1117
  }
1063
1118
  async function fetchMemoryFields(cfg, id) {
1064
1119
  const client = await makeClient(cfg);
1065
- const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
1120
+ const { data } = await client.GET("/memories/{memoryId}", {
1121
+ params: { path: { memoryId: id } }
1122
+ });
1066
1123
  const env = data;
1067
1124
  const m = env?.item ?? env;
1068
1125
  if (!m) return null;
1069
1126
  const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
1070
- return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
1127
+ return {
1128
+ text: m.text,
1129
+ title: m.title,
1130
+ tags: m.tags,
1131
+ version: Number.isFinite(version) ? version : void 0
1132
+ };
1071
1133
  }
1072
- async function resolveInstruction(cfg, section, personalWorkspaceId) {
1073
- const client = await makeClient(cfg);
1074
- for (const artifact of section.artifacts) {
1075
- const tags = parseTagArtifactId(artifact.id);
1076
- if (!tags) continue;
1134
+ async function findTemplateByCandidates(client, candidates) {
1135
+ for (let i = 0; i < candidates.length; i++) {
1136
+ const tags = candidates[i];
1077
1137
  const { data } = await client.POST("/memories/search", {
1078
- body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
1138
+ body: {
1139
+ query: null,
1140
+ textQuery: null,
1141
+ semanticQuery: null,
1142
+ hybrid: false,
1143
+ limit: 1,
1144
+ includeArchived: false,
1145
+ includeSystem: false,
1146
+ tags
1147
+ }
1079
1148
  });
1080
1149
  const hits = data?.items ?? [];
1081
1150
  if (hits.length === 0) continue;
1082
- const templateId = hits[0].id;
1151
+ return { id: hits[0].id, tags, via: i === 0 ? "preferred" : "fallback" };
1152
+ }
1153
+ return null;
1154
+ }
1155
+ function attemptedTagSets(section) {
1156
+ return section.artifacts.flatMap(
1157
+ (artifact) => parseTagArtifactCandidates(artifact.id) ?? []
1158
+ );
1159
+ }
1160
+ async function resolveInstruction(cfg, section, personalWorkspaceId) {
1161
+ const client = await makeClient(cfg);
1162
+ for (const artifact of section.artifacts) {
1163
+ const candidates = parseTagArtifactCandidates(artifact.id);
1164
+ if (!candidates) continue;
1165
+ const found = await findTemplateByCandidates(client, candidates);
1166
+ if (!found) continue;
1167
+ const templateId = found.id;
1083
1168
  const template = await fetchMemoryFields(cfg, templateId);
1084
- if (typeof template?.text !== "string" || template.text.length === 0) continue;
1085
- const templateTags = template.tags ?? tags;
1169
+ if (typeof template?.text !== "string" || template.text.length === 0)
1170
+ continue;
1171
+ const templateTags = template.tags ?? found.tags;
1086
1172
  if (personalWorkspaceId) {
1087
1173
  const { data: ovr } = await client.POST("/memories/search", {
1088
- body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
1174
+ body: {
1175
+ query: null,
1176
+ textQuery: null,
1177
+ semanticQuery: null,
1178
+ hybrid: false,
1179
+ limit: 1,
1180
+ includeArchived: false,
1181
+ includeSystem: false,
1182
+ tags: [
1183
+ "sechroom:role:override",
1184
+ `sechroom:template-ref:${templateId}`
1185
+ ],
1186
+ owner: { type: "Workspace", id: personalWorkspaceId }
1187
+ }
1089
1188
  });
1090
1189
  const ovrHits = ovr?.items ?? [];
1091
1190
  if (ovrHits.length > 0) {
1092
1191
  const override = await fetchMemoryFields(cfg, ovrHits[0].id);
1093
1192
  if (typeof override?.text === "string" && override.text.length > 0) {
1094
- return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
1193
+ return {
1194
+ title: override.title ?? template.title ?? artifact.title,
1195
+ body: override.text,
1196
+ source: "override",
1197
+ templateId,
1198
+ templateTags,
1199
+ resolvedVia: found.via,
1200
+ resolvedTags: found.tags,
1201
+ sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}`
1202
+ };
1095
1203
  }
1096
1204
  }
1097
1205
  }
1098
- return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
1206
+ return {
1207
+ title: template.title ?? artifact.title,
1208
+ body: template.text,
1209
+ source: "template",
1210
+ templateId,
1211
+ templateTags,
1212
+ resolvedVia: found.via,
1213
+ resolvedTags: found.tags,
1214
+ sourceRef: `${templateId}@v${template.version ?? 1}`
1215
+ };
1099
1216
  }
1100
1217
  return null;
1101
1218
  }
@@ -1107,8 +1224,10 @@ async function resolveWorkspaceConventions(cfg, section) {
1107
1224
  const mem = await fetchMemoryFields(cfg, artifact.id);
1108
1225
  if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
1109
1226
  const ref = `${artifact.id}@v${mem.version ?? 1}`;
1110
- parts.push(`<!-- @sechroom/cli:section source=${ref} -->
1111
- ${mem.text.trim()}`);
1227
+ parts.push(
1228
+ `<!-- @sechroom/cli:section source=${ref} -->
1229
+ ${mem.text.trim()}`
1230
+ );
1112
1231
  refs.push(ref);
1113
1232
  }
1114
1233
  }
@@ -1120,7 +1239,10 @@ async function createOverride(cfg, template, personalWorkspaceId) {
1120
1239
  const overrideTags = template.templateTags.filter(
1121
1240
  (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
1122
1241
  );
1123
- overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
1242
+ overrideTags.push(
1243
+ "sechroom:role:override",
1244
+ `sechroom:template-ref:${template.templateId}`
1245
+ );
1124
1246
  const { error } = await client.POST("/memories", {
1125
1247
  body: {
1126
1248
  text: template.body,
@@ -1134,7 +1256,8 @@ async function createOverride(cfg, template, personalWorkspaceId) {
1134
1256
  owner: { type: "Workspace", id: personalWorkspaceId }
1135
1257
  }
1136
1258
  });
1137
- if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
1259
+ if (error)
1260
+ throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
1138
1261
  }
1139
1262
 
1140
1263
  // src/setup/skill-resolution.ts
@@ -1248,6 +1371,89 @@ function resolveReferenceSet(rows, surface) {
1248
1371
  return resolveReferences(rows.systemRows, rows.personalRows, surface);
1249
1372
  }
1250
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
+
1251
1457
  // src/setup/materialise.ts
1252
1458
  var CLIENT_SURFACE = {
1253
1459
  claude: "claude-code",
@@ -1256,8 +1462,8 @@ var CLIENT_SURFACE = {
1256
1462
  function writeSkills(dir, skills, surface) {
1257
1463
  const written = [];
1258
1464
  for (const s of skills) {
1259
- mkdirSync3(join4(dir, s.name), { recursive: true });
1260
- 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");
1261
1467
  written.push(s.name);
1262
1468
  }
1263
1469
  if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -1307,14 +1513,17 @@ function codexAgentToml(agent) {
1307
1513
  ""
1308
1514
  ].join("\n");
1309
1515
  }
1516
+ function agentFileName(name, surface) {
1517
+ return `${name}.${surface === CLIENT_SURFACE.codex ? "toml" : "md"}`;
1518
+ }
1310
1519
  function writeAgents(dir, agents, surface) {
1311
1520
  if (agents.length) mkdirSync3(dir, { recursive: true });
1312
1521
  const written = [];
1313
1522
  for (const a of agents) {
1314
1523
  const codex = surface === CLIENT_SURFACE.codex;
1315
- const file = `${a.name}.${codex ? "toml" : "md"}`;
1524
+ const file = agentFileName(a.name, surface);
1316
1525
  const body = codex ? codexAgentToml(a) : a.body.endsWith("\n") ? a.body : a.body + "\n";
1317
- writeFileSync3(join4(dir, file), body);
1526
+ writeFileSync3(join5(dir, file), body);
1318
1527
  written.push(file);
1319
1528
  }
1320
1529
  if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -1323,10 +1532,10 @@ function writeAgents(dir, agents, surface) {
1323
1532
  function writeReferencesIntoSkillDirs(dir, skills, refs) {
1324
1533
  if (!refs.length || !skills.length) return [];
1325
1534
  for (const s of skills) {
1326
- const refDir = join4(dir, s.name, "references");
1535
+ const refDir = join5(dir, s.name, "references");
1327
1536
  mkdirSync3(refDir, { recursive: true });
1328
1537
  for (const r of refs) {
1329
- 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");
1330
1539
  }
1331
1540
  }
1332
1541
  return refs.map((r) => r.name);
@@ -1336,6 +1545,8 @@ var SKILL_SPEC = {
1336
1545
  dir: skillsDir,
1337
1546
  resolve: resolveSkillSet,
1338
1547
  write: writeSkills,
1548
+ lockNames: (items) => items.map((i) => i.name),
1549
+ layout: SKILL_LAYOUT,
1339
1550
  supportsCodex: true
1340
1551
  };
1341
1552
  var AGENT_SPEC = {
@@ -1343,8 +1554,18 @@ var AGENT_SPEC = {
1343
1554
  dir: agentsDir,
1344
1555
  resolve: resolveAgentSet,
1345
1556
  write: writeAgents,
1557
+ lockNames: (items, surface) => items.map((i) => agentFileName(i.name, surface)),
1558
+ layout: AGENT_LAYOUT,
1346
1559
  supportsCodex: true
1347
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
+ }
1348
1569
  function scopeOf(opts) {
1349
1570
  return opts.local ? "project" : resolveScope(opts.scope);
1350
1571
  }
@@ -1424,8 +1645,16 @@ async function runInstall(spec, cmd, opts) {
1424
1645
  validateCodexWorkerDependencies(items, resolveAgentSet(rows, t.surface));
1425
1646
  }
1426
1647
  const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
1427
- 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);
1428
1656
  const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
1657
+ const pruned = dryRun ? orphans : pruneItems(t.dir, orphans);
1429
1658
  return {
1430
1659
  client: t.client,
1431
1660
  surface: t.surface,
@@ -1436,7 +1665,8 @@ async function runInstall(spec, cmd, opts) {
1436
1665
  items: items.map(({ name, source }) => ({ name, source })),
1437
1666
  referenceItems: refs.map(({ name, source }) => ({ name, source })),
1438
1667
  written,
1439
- refsWritten
1668
+ refsWritten,
1669
+ pruned
1440
1670
  };
1441
1671
  });
1442
1672
  if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
@@ -1452,6 +1682,12 @@ async function runInstall(spec, cmd, opts) {
1452
1682
  console.log(
1453
1683
  `${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
1454
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
+ }
1455
1691
  if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
1456
1692
  }
1457
1693
  }
@@ -1475,7 +1711,7 @@ function runList(spec, cmd, opts) {
1475
1711
  const out = targets.map((t) => {
1476
1712
  const lock = readSkillsLock(t.dir);
1477
1713
  const entries = Object.entries(lock).flatMap(
1478
- ([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)) }))
1479
1715
  );
1480
1716
  return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
1481
1717
  });
@@ -1510,33 +1746,47 @@ function runClean(spec, cmd, opts, slugArg) {
1510
1746
  } catch (err2) {
1511
1747
  return fail(err2.message);
1512
1748
  }
1749
+ const pruneOrphans = Boolean(opts.pruneOrphans);
1513
1750
  const cleaned = [];
1514
1751
  const missing = [];
1515
1752
  for (const t of targets) {
1516
1753
  const lock = readSkillsLock(t.dir);
1517
1754
  const entry = lock[slug2];
1518
- if (!entry) {
1519
- missing.push(join4(t.dir, SKILLS_LOCK));
1755
+ if (!entry && !pruneOrphans) {
1756
+ missing.push(join5(t.dir, SKILLS_LOCK));
1520
1757
  continue;
1521
1758
  }
1522
1759
  const removed = [];
1523
- for (const name of entry.skills) {
1524
- const p = join4(t.dir, name);
1760
+ for (const name of entry?.skills ?? []) {
1761
+ const p = join5(t.dir, name);
1525
1762
  if (existsSync3(p)) {
1526
- rmSync2(p, { recursive: true, force: true });
1763
+ rmSync3(p, { recursive: true, force: true });
1527
1764
  removed.push(name);
1528
1765
  }
1529
1766
  }
1530
- delete lock[slug2];
1531
- writeSkillsLock(t.dir, lock);
1532
- 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 });
1533
1777
  }
1534
1778
  if (cleaned.length === 0) {
1535
1779
  return fail(`No materialised ${spec.kind}s recorded for '${slug2}' in ${missing.join(", ")}.`);
1536
1780
  }
1537
- 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);
1538
1782
  for (const c of cleaned) {
1539
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
+ }
1540
1790
  }
1541
1791
  }
1542
1792
 
@@ -1560,12 +1810,12 @@ target:gpt-codex-agent), the dispatchable workers your loop skills call
1560
1810
  );
1561
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));
1562
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));
1563
- 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));
1564
1814
  }
1565
1815
 
1566
1816
  // src/commands/channel.ts
1567
- import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
1568
- import { dirname as dirname9, join as join12 } from "path";
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";
1569
1819
  import {
1570
1820
  HttpTransportType,
1571
1821
  HubConnectionBuilder
@@ -1749,6 +1999,44 @@ async function runDriverLoop(ports, options) {
1749
1999
  }
1750
2000
  throw error;
1751
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
+ }
1752
2040
  const stopHeartbeat = ports.startLeaseHeartbeat(claim);
1753
2041
  let result;
1754
2042
  try {
@@ -1833,15 +2121,19 @@ Delivery: FAILED unexpectedly (${String(e)}) \u2014 changes remain in the execut
1833
2121
  return summary;
1834
2122
  }
1835
2123
  function closeoutText(task, result) {
2124
+ const targetEvidence = task.reviewTargetEvidence ? `
2125
+
2126
+ Review target preflight evidence:
2127
+ ${task.reviewTargetEvidence}` : "";
1836
2128
  if (!result.packet)
1837
2129
  return `Driven codex run ended without a sechroom_closeout packet (soft-fail). Last agent message:
1838
2130
 
1839
- ${result.lastAgentMessage || "(none)"}`;
2131
+ ${result.lastAgentMessage || "(none)"}${targetEvidence}`;
1840
2132
  const evidence = result.packet.evidence?.length ? `
1841
2133
 
1842
2134
  Evidence:
1843
2135
  ${result.packet.evidence.map((e) => `- ${e}`).join("\n")}` : "";
1844
- return `${result.packet.summary}${evidence}
2136
+ return `${result.packet.summary}${evidence}${targetEvidence}
1845
2137
 
1846
2138
  (terminal_status: ${result.packet.terminal_status}; driven by sechroom executor run.)`;
1847
2139
  }
@@ -1883,7 +2175,8 @@ async function materializeClaimedTask(request, memoryId) {
1883
2175
  }
1884
2176
  return {
1885
2177
  title: card.title ?? memoryId,
1886
- text: assemblePrompt(card, components)
2178
+ text: assemblePrompt(card, components),
2179
+ reviewExecution: card.reviewExecution ?? void 0
1887
2180
  };
1888
2181
  }
1889
2182
  function validatePackage(value, expectedSlug, expectedVersion) {
@@ -1917,6 +2210,11 @@ ${card.task.boundaries}`,
1917
2210
  `## Closeout
1918
2211
  ${card.task.closeout}`
1919
2212
  ];
2213
+ if (card.reviewExecution)
2214
+ sections.push(
2215
+ `## Review target
2216
+ ${renderReviewExecution(card.reviewExecution)}`
2217
+ );
1920
2218
  if (components.length > 0)
1921
2219
  sections.push(
1922
2220
  `## Task context
@@ -1924,6 +2222,17 @@ ${components.map(renderComponent).join("\n\n")}`
1924
2222
  );
1925
2223
  return sections.join("\n\n");
1926
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
+ }
1927
2236
  function renderComponent(component) {
1928
2237
  return [
1929
2238
  `<!-- sechroom-task-context component=${JSON.stringify(component.slug)} id=${JSON.stringify(component.sourceId)} sourceVersion=${component.sourceVersion} -->`,
@@ -1934,21 +2243,22 @@ function renderComponent(component) {
1934
2243
 
1935
2244
  // src/commands/executor.ts
1936
2245
  import { execFileSync } from "child_process";
1937
- import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
1938
- 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";
1939
2249
 
1940
2250
  // src/sem.ts
1941
- import { dirname as dirname2, join as join5 } from "path";
1942
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1943
- 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");
1944
2254
  var STATE_DIR_NAME2 = ".sechroom";
1945
2255
  function localSemPath(cwd = process.cwd()) {
1946
- return join5(cwd, SEM_FILE);
2256
+ return join6(cwd, SEM_FILE);
1947
2257
  }
1948
2258
  function resolveSemPathForRead(start = process.cwd()) {
1949
2259
  let dir = start;
1950
2260
  while (true) {
1951
- const candidate = join5(dir, SEM_FILE);
2261
+ const candidate = join6(dir, SEM_FILE);
1952
2262
  if (existsSync4(candidate)) return candidate;
1953
2263
  const parent = dirname2(dir);
1954
2264
  if (parent === dir) return void 0;
@@ -1960,7 +2270,7 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1960
2270
  let dir = start;
1961
2271
  let gitPath;
1962
2272
  for (; ; ) {
1963
- const candidate = join5(dir, ".git");
2273
+ const candidate = join6(dir, ".git");
1964
2274
  if (existsSync4(candidate)) {
1965
2275
  gitPath = candidate;
1966
2276
  break;
@@ -1969,14 +2279,14 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1969
2279
  if (parent === dir) break;
1970
2280
  dir = parent;
1971
2281
  }
1972
- if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1973
- const gitFile = readFileSync3(gitPath, "utf8");
2282
+ if (!gitPath || statSync2(gitPath).isDirectory()) return lane;
2283
+ const gitFile = readFileSync4(gitPath, "utf8");
1974
2284
  const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1975
2285
  if (!common) return lane;
1976
- const worktreesDir = join5(common[1], "worktrees");
1977
- const siblings = readdirSync(worktreesDir).filter((n) => {
2286
+ const worktreesDir = join6(common[1], "worktrees");
2287
+ const siblings = readdirSync2(worktreesDir).filter((n) => {
1978
2288
  try {
1979
- return statSync(join5(worktreesDir, n)).isDirectory();
2289
+ return statSync2(join6(worktreesDir, n)).isDirectory();
1980
2290
  } catch {
1981
2291
  return false;
1982
2292
  }
@@ -1998,10 +2308,10 @@ function serializeSem(values) {
1998
2308
  function readSem(path) {
1999
2309
  const p = path ?? resolveSemPathForRead();
2000
2310
  if (!p || !existsSync4(p)) return void 0;
2001
- return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
2311
+ return { path: p, values: parseLaneJson(readFileSync4(p, "utf8")) };
2002
2312
  }
2003
2313
  function readLocalSemValues(cwd = process.cwd()) {
2004
- const next = join5(cwd, SEM_FILE);
2314
+ const next = join6(cwd, SEM_FILE);
2005
2315
  if (existsSync4(next)) return readSem(next)?.values ?? {};
2006
2316
  return {};
2007
2317
  }
@@ -2047,7 +2357,7 @@ var CONTINUITY_SCAFFOLD = JSON.stringify(
2047
2357
  ) + "\n";
2048
2358
  function ensureContinuityScaffold(semPath) {
2049
2359
  try {
2050
- const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
2360
+ const target = join6(dirname2(semPath), CONTINUITY_FILE_NAME);
2051
2361
  if (existsSync4(target)) return;
2052
2362
  writeFileSync4(target, CONTINUITY_SCAFFOLD);
2053
2363
  } catch {
@@ -2062,7 +2372,7 @@ function ignoresSem(content) {
2062
2372
  function inGitRepo(startDir) {
2063
2373
  let dir = startDir;
2064
2374
  for (; ; ) {
2065
- if (existsSync4(join5(dir, ".git"))) return true;
2375
+ if (existsSync4(join6(dir, ".git"))) return true;
2066
2376
  const parent = dirname2(dir);
2067
2377
  if (parent === dir) return false;
2068
2378
  dir = parent;
@@ -2071,11 +2381,11 @@ function inGitRepo(startDir) {
2071
2381
  function resolveGitignoreTarget(startDir) {
2072
2382
  let dir = startDir;
2073
2383
  for (; ; ) {
2074
- const gi = join5(dir, ".gitignore");
2384
+ const gi = join6(dir, ".gitignore");
2075
2385
  if (existsSync4(gi)) return { path: gi, exists: true };
2076
2386
  const parent = dirname2(dir);
2077
- if (existsSync4(join5(dir, ".git")) || parent === dir) {
2078
- return { path: join5(startDir, ".gitignore"), exists: false };
2387
+ if (existsSync4(join6(dir, ".git")) || parent === dir) {
2388
+ return { path: join6(startDir, ".gitignore"), exists: false };
2079
2389
  }
2080
2390
  dir = parent;
2081
2391
  }
@@ -2086,7 +2396,7 @@ function ensureSemIgnored(semPath) {
2086
2396
  if (!inGitRepo(checkoutDir)) return;
2087
2397
  const target = resolveGitignoreTarget(checkoutDir);
2088
2398
  if (target.exists) {
2089
- const content = readFileSync3(target.path, "utf8");
2399
+ const content = readFileSync4(target.path, "utf8");
2090
2400
  if (ignoresSem(content)) return;
2091
2401
  const sep2 = content.length === 0 || content.endsWith("\n") ? "" : "\n";
2092
2402
  appendFileSync(target.path, `${sep2}${STATE_DIR_IGNORE}
@@ -2100,7 +2410,7 @@ function ensureSemIgnored(semPath) {
2100
2410
  }
2101
2411
 
2102
2412
  // src/commands/executor-run.ts
2103
- import { join as join10, resolve as resolve3 } from "path";
2413
+ import { join as join11, resolve as resolve3 } from "path";
2104
2414
 
2105
2415
  // src/executor-run/usage.ts
2106
2416
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
@@ -2719,7 +3029,9 @@ var CodexAppServer = class {
2719
3029
  modelId,
2720
3030
  executorInstanceId: this.options.executorInstanceId,
2721
3031
  leaseId: telemetry?.leaseId ?? null,
2722
- turnId: turnId || null
3032
+ turnId: turnId || null,
3033
+ originatingReviewId: telemetry?.originatingReviewId ?? null,
3034
+ reviewRound: telemetry?.reviewRound ?? null
2723
3035
  });
2724
3036
  this.onNotification = (msg) => {
2725
3037
  if (msg.method === "thread/tokenUsage/updated") {
@@ -3243,8 +3555,8 @@ import {
3243
3555
  closeSync,
3244
3556
  mkdirSync as mkdirSync6,
3245
3557
  openSync,
3246
- readFileSync as readFileSync4,
3247
- rmSync as rmSync3,
3558
+ readFileSync as readFileSync5,
3559
+ rmSync as rmSync4,
3248
3560
  writeFileSync as writeFileSync5
3249
3561
  } from "fs";
3250
3562
  import { readFile } from "fs/promises";
@@ -3253,7 +3565,7 @@ import {
3253
3565
  createServer as createServer2
3254
3566
  } from "net";
3255
3567
  import { tmpdir } from "os";
3256
- import { dirname as dirname4, join as join6, resolve } from "path";
3568
+ import { dirname as dirname4, join as join7, resolve } from "path";
3257
3569
  var DEFAULT_RESTART_POLICY = {
3258
3570
  enabled: true,
3259
3571
  maxRetries: 10,
@@ -4110,7 +4422,7 @@ function isProcessAlive(pid) {
4110
4422
  function readPidFile(path) {
4111
4423
  let raw;
4112
4424
  try {
4113
- raw = readFileSync4(path, "utf8");
4425
+ raw = readFileSync5(path, "utf8");
4114
4426
  } catch (error) {
4115
4427
  if (error.code === "ENOENT") return void 0;
4116
4428
  throw error;
@@ -4124,7 +4436,7 @@ function claimPidFile(path, options = {}) {
4124
4436
  });
4125
4437
  let raw;
4126
4438
  try {
4127
- raw = readFileSync4(path, "utf8");
4439
+ raw = readFileSync5(path, "utf8");
4128
4440
  } catch (error) {
4129
4441
  if (error.code === "ENOENT") return;
4130
4442
  throw error;
@@ -4138,7 +4450,7 @@ function claimPidFile(path, options = {}) {
4138
4450
  log(
4139
4451
  `stale pid-file ${path} (${valid ? `pid ${pid}` : "unparseable"} not running) \u2014 reclaiming`
4140
4452
  );
4141
- rmSync3(path, { force: true });
4453
+ rmSync4(path, { force: true });
4142
4454
  }
4143
4455
  function writePidFile(path, pid) {
4144
4456
  mkdirSync6(dirname4(path), { recursive: true });
@@ -4148,7 +4460,7 @@ function writePidFile(path, pid) {
4148
4460
  function removePidFileIfOwned(path, pid, log) {
4149
4461
  if (readPidFile(path) !== pid) return;
4150
4462
  try {
4151
- rmSync3(path, { force: true });
4463
+ rmSync4(path, { force: true });
4152
4464
  } catch (error) {
4153
4465
  log?.(`pid-file ${path} cleanup failed: ${String(error)}`);
4154
4466
  }
@@ -4198,12 +4510,12 @@ function launchDetachedSupervisor(options) {
4198
4510
  }
4199
4511
  function controlSocketPath(pidFile) {
4200
4512
  const hash = createHash2("sha256").update(resolve(pidFile)).digest("hex").slice(0, 16);
4201
- return join6(tmpdir(), `sechroom-fleet-${hash}.sock`);
4513
+ return join7(tmpdir(), `sechroom-fleet-${hash}.sock`);
4202
4514
  }
4203
4515
  function startControlServer(socketPath, handlers, options = {}) {
4204
4516
  const log = options.log ?? (() => {
4205
4517
  });
4206
- rmSync3(socketPath, { force: true });
4518
+ rmSync4(socketPath, { force: true });
4207
4519
  const dispatch = (req) => {
4208
4520
  switch (req.command) {
4209
4521
  case "status":
@@ -4265,7 +4577,7 @@ function startControlServer(socketPath, handlers, options = {}) {
4265
4577
  socketPath,
4266
4578
  close: () => new Promise((res) => {
4267
4579
  server.close(() => {
4268
- rmSync3(socketPath, { force: true });
4580
+ rmSync4(socketPath, { force: true });
4269
4581
  res();
4270
4582
  });
4271
4583
  })
@@ -4344,29 +4656,88 @@ function sendControlCommand(socketPath, request, options = {}) {
4344
4656
  });
4345
4657
  }
4346
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
+
4347
4718
  // src/commands/telemetry.ts
4348
4719
  import {
4349
4720
  existsSync as existsSync7,
4350
4721
  mkdirSync as mkdirSync8,
4351
- readFileSync as readFileSync6,
4352
- rmSync as rmSync4,
4722
+ readFileSync as readFileSync7,
4723
+ rmSync as rmSync5,
4353
4724
  writeFileSync as writeFileSync7
4354
4725
  } from "fs";
4355
4726
  import { homedir as homedir4 } from "os";
4356
- 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";
4357
4728
 
4358
4729
  // src/commands/hook-install.ts
4359
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4360
- 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";
4361
4732
 
4362
4733
  // src/setup/clients.ts
4363
4734
  import { existsSync as existsSync5 } from "fs";
4364
4735
  import { homedir as homedir3 } from "os";
4365
- import { dirname as dirname5, join as join7 } from "path";
4736
+ import { dirname as dirname5, join as join8 } from "path";
4366
4737
  function claudeDesktopConfigPath(home) {
4367
4738
  switch (process.platform) {
4368
4739
  case "darwin":
4369
- return join7(
4740
+ return join8(
4370
4741
  home,
4371
4742
  "Library",
4372
4743
  "Application Support",
@@ -4374,19 +4745,20 @@ function claudeDesktopConfigPath(home) {
4374
4745
  "claude_desktop_config.json"
4375
4746
  );
4376
4747
  case "win32":
4377
- return join7(
4378
- process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
4748
+ return join8(
4749
+ process.env.APPDATA ?? join8(home, "AppData", "Roaming"),
4379
4750
  "Claude",
4380
4751
  "claude_desktop_config.json"
4381
4752
  );
4382
4753
  default:
4383
- return join7(home, ".config", "Claude", "claude_desktop_config.json");
4754
+ return join8(home, ".config", "Claude", "claude_desktop_config.json");
4384
4755
  }
4385
4756
  }
4386
4757
  function clientTargets(cwd, opts = {}) {
4387
4758
  const home = homedir3();
4388
- const claudeDir = opts.claudeDir ?? join7(home, ".claude");
4389
- 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;
4390
4762
  return {
4391
4763
  "claude-code": {
4392
4764
  key: "claude-code",
@@ -4394,10 +4766,10 @@ function clientTargets(cwd, opts = {}) {
4394
4766
  mcp: {
4395
4767
  surfaceKey: "claude-code",
4396
4768
  sectionType: SectionType.McpConfig,
4397
- path: join7(cwd, ".mcp.json"),
4769
+ path: join8(cwd, ".mcp.json"),
4398
4770
  format: "json"
4399
4771
  },
4400
- instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
4772
+ instruction: { surfaceKey: "claude-code", path: join8(cwd, "CLAUDE.md") }
4401
4773
  },
4402
4774
  "claude-desktop": {
4403
4775
  key: "claude-desktop",
@@ -4410,19 +4782,19 @@ function clientTargets(cwd, opts = {}) {
4410
4782
  },
4411
4783
  instruction: {
4412
4784
  surfaceKey: "claude-desktop",
4413
- path: join7(claudeDir, "CLAUDE.md")
4785
+ path: join8(claudeDir, "CLAUDE.md")
4414
4786
  }
4415
4787
  },
4416
4788
  codex: {
4417
4789
  key: "codex",
4418
4790
  label: "Codex CLI",
4419
- mcp: codexHome ? {
4791
+ mcp: codexConfigPath ? {
4420
4792
  surfaceKey: "chatgpt",
4421
4793
  sectionType: SectionType.McpConfigToml,
4422
- path: join7(codexHome, "config.toml"),
4794
+ path: codexConfigPath,
4423
4795
  format: "toml"
4424
4796
  } : null,
4425
- instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4797
+ instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
4426
4798
  },
4427
4799
  cursor: {
4428
4800
  key: "cursor",
@@ -4430,10 +4802,10 @@ function clientTargets(cwd, opts = {}) {
4430
4802
  mcp: {
4431
4803
  surfaceKey: "claude-code",
4432
4804
  sectionType: SectionType.McpConfig,
4433
- path: join7(cwd, ".cursor", "mcp.json"),
4805
+ path: join8(cwd, ".cursor", "mcp.json"),
4434
4806
  format: "json"
4435
4807
  },
4436
- instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4808
+ instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
4437
4809
  },
4438
4810
  antigravity: {
4439
4811
  key: "antigravity",
@@ -4447,10 +4819,10 @@ function clientTargets(cwd, opts = {}) {
4447
4819
  mcp: {
4448
4820
  surfaceKey: "antigravity",
4449
4821
  sectionType: SectionType.McpConfig,
4450
- path: join7(home, ".gemini", "config", "mcp_config.json"),
4822
+ path: join8(home, ".gemini", "config", "mcp_config.json"),
4451
4823
  format: "json"
4452
4824
  },
4453
- instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
4825
+ instruction: { surfaceKey: "antigravity", path: join8(cwd, "AGENTS.md") }
4454
4826
  }
4455
4827
  };
4456
4828
  }
@@ -4470,9 +4842,9 @@ function detectInstalledClients(cwd) {
4470
4842
  if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
4471
4843
  detected.push("claude-desktop");
4472
4844
  if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
4473
- if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
4845
+ if (existsSync5(join8(home, ".cursor")) || existsSync5(join8(cwd, ".cursor")))
4474
4846
  detected.push("cursor");
4475
- if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
4847
+ if (existsSync5(join8(home, ".gemini"))) detected.push("antigravity");
4476
4848
  return detected;
4477
4849
  }
4478
4850
 
@@ -4516,12 +4888,12 @@ function mergeHooks(config2, commands) {
4516
4888
  }
4517
4889
  function readJsonConfig2(path) {
4518
4890
  if (!existsSync6(path)) return {};
4519
- const raw = readFileSync5(path, "utf8");
4891
+ const raw = readFileSync6(path, "utf8");
4520
4892
  if (!raw.trim()) return {};
4521
4893
  return JSON.parse(raw);
4522
4894
  }
4523
4895
  function installHooksJson(path, commands, dryRun) {
4524
- const existed = existsSync6(path) && readFileSync5(path, "utf8").trim().length > 0;
4896
+ const existed = existsSync6(path) && readFileSync6(path, "utf8").trim().length > 0;
4525
4897
  const config2 = readJsonConfig2(path);
4526
4898
  const added = mergeHooks(config2, commands);
4527
4899
  if (added === 0 && existed) return { path, status: "current" };
@@ -4532,12 +4904,12 @@ function installHooksJson(path, commands, dryRun) {
4532
4904
  return { path, status: existed ? "merged" : "created" };
4533
4905
  }
4534
4906
  function installClaudeCommands(claudeDir, commands, dryRun) {
4535
- return installHooksJson(join8(claudeDir, "settings.json"), commands, dryRun);
4907
+ return installHooksJson(join9(claudeDir, "settings.json"), commands, dryRun);
4536
4908
  }
4537
4909
  function installCodexCommands(codexHome, commands, dryRun) {
4538
4910
  return [
4539
- installHooksJson(join8(codexHome, "hooks.json"), commands, dryRun),
4540
- installCodexFeatureFlag(join8(codexHome, "config.toml"), dryRun)
4911
+ installHooksJson(join9(codexHome, "hooks.json"), commands, dryRun),
4912
+ installCodexFeatureFlag(join9(codexHome, "config.toml"), dryRun)
4541
4913
  ];
4542
4914
  }
4543
4915
  function ensureCodexFeaturesHooks(content) {
@@ -4562,7 +4934,7 @@ function ensureCodexFeaturesHooks(content) {
4562
4934
  }
4563
4935
  function installCodexFeatureFlag(path, dryRun) {
4564
4936
  const existed = existsSync6(path);
4565
- const content = existed ? readFileSync5(path, "utf8") : "";
4937
+ const content = existed ? readFileSync6(path, "utf8") : "";
4566
4938
  const { next, changed } = ensureCodexFeaturesHooks(content);
4567
4939
  if (!changed) return { path, status: "current" };
4568
4940
  if (!dryRun) {
@@ -4592,11 +4964,11 @@ function installHookSurfaces(surfaces, opts) {
4592
4964
  const out = [];
4593
4965
  for (const surface of surfaces) {
4594
4966
  if (surface === "claude") {
4595
- const path = join8(opts.claudeDir, "settings.json");
4967
+ const path = join9(opts.claudeDir, "settings.json");
4596
4968
  out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
4597
4969
  } else {
4598
- const hooksJson = installHooksJson(join8(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
4599
- 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);
4600
4972
  out.push({ surface, results: [hooksJson, featureFlag] });
4601
4973
  }
4602
4974
  }
@@ -4616,7 +4988,7 @@ function isSechroomOnPath() {
4616
4988
  for (const dir of pathEnv.split(delimiter)) {
4617
4989
  if (!dir) continue;
4618
4990
  for (const name of names) {
4619
- if (existsSync6(join8(dir, name))) return true;
4991
+ if (existsSync6(join9(dir, name))) return true;
4620
4992
  }
4621
4993
  }
4622
4994
  return false;
@@ -4661,6 +5033,13 @@ function registerTelemetry(program2) {
4661
5033
  ).option("--text <s>", "Raw/parsed payload text").option("--approval <state>", "Approval gate state (approval events)").option(
4662
5034
  "--verdict <v>",
4663
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
4664
5043
  ).action(async (opts, cmd) => {
4665
5044
  const json = Boolean(cmd.optsWithGlobals().json);
4666
5045
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -4673,7 +5052,9 @@ function registerTelemetry(program2) {
4673
5052
  contextWindow: opts.contextWindow ?? null,
4674
5053
  text: opts.text ?? null,
4675
5054
  approvalState: opts.approval ?? null,
4676
- verdict: opts.verdict ?? null
5055
+ verdict: opts.verdict ?? null,
5056
+ originatingReviewId: opts.originatingReviewId,
5057
+ reviewRound: opts.reviewRound
4677
5058
  };
4678
5059
  let body;
4679
5060
  try {
@@ -4714,14 +5095,23 @@ function registerTelemetry(program2) {
4714
5095
  ).requiredOption(
4715
5096
  "--decomposition <id>",
4716
5097
  "Decomposition id this session executes"
4717
- ).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) => {
4718
5106
  const json = Boolean(cmd.optsWithGlobals().json);
4719
- const dir = join9(process.cwd(), ".sechroom");
5107
+ const dir = join10(process.cwd(), ".sechroom");
4720
5108
  mkdirSync8(dir, { recursive: true });
4721
- const path = join9(dir, BINDING_FILE);
5109
+ const path = join10(dir, BINDING_FILE);
4722
5110
  const binding = {
4723
5111
  decompositionId: opts.decomposition,
4724
- taskId: opts.task
5112
+ taskId: opts.task,
5113
+ originatingReviewId: opts.originatingReviewId,
5114
+ reviewRound: opts.reviewRound
4725
5115
  };
4726
5116
  writeFileSync7(path, JSON.stringify(binding, null, 2) + "\n");
4727
5117
  ensureStateDirIgnored(process.cwd());
@@ -4738,9 +5128,9 @@ function registerTelemetry(program2) {
4738
5128
  });
4739
5129
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
4740
5130
  const json = Boolean(cmd.optsWithGlobals().json);
4741
- const path = join9(process.cwd(), ".sechroom", BINDING_FILE);
5131
+ const path = join10(process.cwd(), ".sechroom", BINDING_FILE);
4742
5132
  const existed = existsSync7(path);
4743
- if (existed) rmSync4(path);
5133
+ if (existed) rmSync5(path);
4744
5134
  if (json) emit({ unbound: existed, path }, true);
4745
5135
  else
4746
5136
  process.stdout.write(
@@ -4766,7 +5156,9 @@ function registerTelemetry(program2) {
4766
5156
  input,
4767
5157
  usage,
4768
5158
  binding.taskId,
4769
- configRoot
5159
+ configRoot,
5160
+ binding.originatingReviewId ?? null,
5161
+ binding.reviewRound ?? null
4770
5162
  );
4771
5163
  if (events.length === 0) return process.exit(0);
4772
5164
  const taskId = await taskIdForHook(cfg, binding);
@@ -4860,11 +5252,11 @@ async function postTelemetry(cfg, decompositionId, events) {
4860
5252
  function findBinding(start) {
4861
5253
  let dir = start;
4862
5254
  for (; ; ) {
4863
- const path = join9(dir, ".sechroom", BINDING_FILE);
5255
+ const path = join10(dir, ".sechroom", BINDING_FILE);
4864
5256
  if (existsSync7(path)) {
4865
5257
  try {
4866
5258
  const b = JSON.parse(
4867
- readFileSync6(path, "utf8")
5259
+ readFileSync7(path, "utf8")
4868
5260
  );
4869
5261
  if (b.decompositionId && b.taskId)
4870
5262
  return {
@@ -4872,6 +5264,8 @@ function findBinding(start) {
4872
5264
  taskId: b.taskId,
4873
5265
  activeTaskCheckedAt: b.activeTaskCheckedAt,
4874
5266
  lifecycleWarning: b.lifecycleWarning,
5267
+ originatingReviewId: b.originatingReviewId,
5268
+ reviewRound: b.reviewRound,
4875
5269
  path
4876
5270
  };
4877
5271
  if (b.decompositionId && b.invalidatedTaskId && b.invalidatedReason === "terminal-task")
@@ -4879,6 +5273,8 @@ function findBinding(start) {
4879
5273
  decompositionId: b.decompositionId,
4880
5274
  invalidatedTaskId: b.invalidatedTaskId,
4881
5275
  invalidatedReason: b.invalidatedReason,
5276
+ originatingReviewId: b.originatingReviewId,
5277
+ reviewRound: b.reviewRound,
4882
5278
  path
4883
5279
  };
4884
5280
  } catch {
@@ -4941,7 +5337,7 @@ async function getTaskLifecycleVerdict(cfg, binding) {
4941
5337
  function markLifecycleWarningIfCurrent(binding) {
4942
5338
  try {
4943
5339
  const current = JSON.parse(
4944
- readFileSync6(binding.path, "utf8")
5340
+ readFileSync7(binding.path, "utf8")
4945
5341
  );
4946
5342
  if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
4947
5343
  return;
@@ -4949,7 +5345,9 @@ function markLifecycleWarningIfCurrent(binding) {
4949
5345
  decompositionId: binding.decompositionId,
4950
5346
  taskId: binding.taskId,
4951
5347
  activeTaskCheckedAt: current.activeTaskCheckedAt,
4952
- lifecycleWarning: "insufficient-permission"
5348
+ lifecycleWarning: "insufficient-permission",
5349
+ originatingReviewId: current.originatingReviewId,
5350
+ reviewRound: current.reviewRound
4953
5351
  };
4954
5352
  writeFileSync7(binding.path, JSON.stringify(warned, null, 2) + "\n");
4955
5353
  } catch {
@@ -4958,14 +5356,16 @@ function markLifecycleWarningIfCurrent(binding) {
4958
5356
  function cacheActiveBindingIfCurrent(binding) {
4959
5357
  try {
4960
5358
  const current = JSON.parse(
4961
- readFileSync6(binding.path, "utf8")
5359
+ readFileSync7(binding.path, "utf8")
4962
5360
  );
4963
5361
  if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
4964
5362
  return;
4965
5363
  const cached = {
4966
5364
  decompositionId: binding.decompositionId,
4967
5365
  taskId: binding.taskId,
4968
- activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
5366
+ activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
5367
+ originatingReviewId: current.originatingReviewId,
5368
+ reviewRound: current.reviewRound
4969
5369
  };
4970
5370
  writeFileSync7(binding.path, JSON.stringify(cached, null, 2) + "\n");
4971
5371
  } catch {
@@ -4974,7 +5374,7 @@ function cacheActiveBindingIfCurrent(binding) {
4974
5374
  function invalidateBindingIfCurrent(binding) {
4975
5375
  try {
4976
5376
  const current = JSON.parse(
4977
- readFileSync6(binding.path, "utf8")
5377
+ readFileSync7(binding.path, "utf8")
4978
5378
  );
4979
5379
  if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
4980
5380
  return;
@@ -4984,7 +5384,9 @@ function invalidateBindingIfCurrent(binding) {
4984
5384
  {
4985
5385
  decompositionId: binding.decompositionId,
4986
5386
  invalidatedTaskId: binding.taskId,
4987
- invalidatedReason: "terminal-task"
5387
+ invalidatedReason: "terminal-task",
5388
+ originatingReviewId: current.originatingReviewId,
5389
+ reviewRound: current.reviewRound
4988
5390
  },
4989
5391
  null,
4990
5392
  2
@@ -4999,7 +5401,7 @@ function parseTranscript(path) {
4999
5401
  let tokensOut = 0;
5000
5402
  let contextUsed = 0;
5001
5403
  let model = "";
5002
- for (const line of readFileSync6(path, "utf8").split("\n")) {
5404
+ for (const line of readFileSync7(path, "utf8").split("\n")) {
5003
5405
  if (!line.trim()) continue;
5004
5406
  let obj;
5005
5407
  try {
@@ -5016,14 +5418,20 @@ function parseTranscript(path) {
5016
5418
  if (obj.message?.model) model = obj.message.model;
5017
5419
  }
5018
5420
  if (tokensIn === 0 && tokensOut === 0) return null;
5019
- 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
+ };
5020
5428
  }
5021
5429
  function windowFor(model, contextUsed = 0) {
5022
5430
  const m = model.toLowerCase();
5023
5431
  if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
5024
5432
  return contextUsed > 2e5 ? 1e6 : 2e5;
5025
5433
  }
5026
- function buildHookEvents(input, usage, taskId, configRoot = null) {
5434
+ function buildHookEvents(input, usage, taskId, configRoot = null, originatingReviewId = null, reviewRound = null) {
5027
5435
  const events = [];
5028
5436
  const base = (kind, over) => ({
5029
5437
  taskId,
@@ -5037,6 +5445,8 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
5037
5445
  verdict: null,
5038
5446
  modelId: null,
5039
5447
  configRoot,
5448
+ originatingReviewId,
5449
+ reviewRound,
5040
5450
  ...over
5041
5451
  });
5042
5452
  if (usage) {
@@ -5065,7 +5475,9 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
5065
5475
  break;
5066
5476
  case "Stop":
5067
5477
  case "SubagentStop":
5068
- events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
5478
+ events.push(
5479
+ base("Terminal", { text: input.last_assistant_message ?? null })
5480
+ );
5069
5481
  break;
5070
5482
  }
5071
5483
  return events;
@@ -5086,7 +5498,7 @@ function resolveClaudeConfigRoot(configuredRoot, transcriptPath) {
5086
5498
  function normalizeClaudeConfigRoot(candidate) {
5087
5499
  const trimmed = candidate?.trim();
5088
5500
  if (!trimmed) return null;
5089
- 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;
5090
5502
  let normalized = resolve2(expanded);
5091
5503
  const rootLength = parse(normalized).root.length;
5092
5504
  while (normalized.length > rootLength && normalized.endsWith(sep))
@@ -5160,10 +5572,10 @@ function registerExecutorRunCommand(executor) {
5160
5572
  fail("--detach and --foreground are mutually exclusive");
5161
5573
  const detach = !foreground;
5162
5574
  const pidFile = resolve3(
5163
- opts.pidFile ? String(opts.pidFile) : join10(process.cwd(), ".sechroom", "fleet.pid")
5575
+ opts.pidFile ? String(opts.pidFile) : join11(process.cwd(), ".sechroom", "fleet.pid")
5164
5576
  );
5165
5577
  const logFile = resolve3(
5166
- opts.logFile ? String(opts.logFile) : join10(process.cwd(), ".sechroom", "fleet.log")
5578
+ opts.logFile ? String(opts.logFile) : join11(process.cwd(), ".sechroom", "fleet.log")
5167
5579
  );
5168
5580
  if (detach && !isDetachedChild) {
5169
5581
  await readFleetConfig(String(opts.config));
@@ -5449,7 +5861,7 @@ function registerExecutorRunCommand(executor) {
5449
5861
  if (excludeTags.length)
5450
5862
  log(`excluding offers tagged: ${excludeTags.join(", ")}`);
5451
5863
  const rootDir = resolve3(String(opts.root));
5452
- const usageLogPath = join10(
5864
+ const usageLogPath = join11(
5453
5865
  rootDir,
5454
5866
  ".sechroom",
5455
5867
  `executor-usage-${located.state.instanceKey.replace(/[^\w.-]/g, "-")}.jsonl`
@@ -5579,6 +5991,7 @@ function registerExecutorRunCommand(executor) {
5579
5991
  },
5580
5992
  claimNext: async () => await fleetInbox.waitForClaim() ?? await claimNext(request, instance.id, log, excludeTags, skipLog),
5581
5993
  loadTask: (memoryId) => materializeClaimedTask(request, memoryId),
5994
+ verifyTaskBinding: (task) => verifyReviewTarget(gitRunner, task),
5582
5995
  startLeaseHeartbeat: (claim) => startLeaseHeartbeat(
5583
5996
  () => request(
5584
5997
  `/me/executor-task-leases/${encodeURIComponent(claim.leaseId)}/heartbeat`,
@@ -5707,7 +6120,7 @@ function registerExecutorRunCommand(executor) {
5707
6120
  }
5708
6121
  function resolveFleetPidFile(flag) {
5709
6122
  return resolve3(
5710
- flag ? String(flag) : join10(process.cwd(), ".sechroom", "fleet.pid")
6123
+ flag ? String(flag) : join11(process.cwd(), ".sechroom", "fleet.pid")
5711
6124
  );
5712
6125
  }
5713
6126
  function requireLiveSupervisor(pidFile) {
@@ -5843,6 +6256,13 @@ var CODEX_EXECUTOR_HOOKS = {
5843
6256
  PreToolUse: EXECUTOR_PULSE_COMMAND,
5844
6257
  Stop: EXECUTOR_PULSE_COMMAND
5845
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
+ }
5846
6266
  function registerExecutor(program2) {
5847
6267
  const executor = program2.command("executor").description(
5848
6268
  "Register and operate a local Claude Code/Codex executor advertisement"
@@ -5955,8 +6375,8 @@ function registerExecutor(program2) {
5955
6375
  fail("refresh-after must be shorter than the TTL");
5956
6376
  const sem = readSem();
5957
6377
  const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
5958
- const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
5959
- 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;
5960
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;
5961
6381
  const state = {
5962
6382
  schemaVersion: 1,
@@ -5985,8 +6405,8 @@ function registerExecutor(program2) {
5985
6405
  }
5986
6406
  const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
5987
6407
  (target) => target.dir
5988
- ) : [join11(checkout, ".claude")];
5989
- 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")];
5990
6410
  const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
5991
6411
  for (const target of hookTargets) {
5992
6412
  const results = surface === "claude" ? [
@@ -6201,6 +6621,29 @@ function registerExecutor(program2) {
6201
6621
  );
6202
6622
  emit(data, Boolean(cmd.optsWithGlobals().json));
6203
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
+ });
6204
6647
  executor.command("proxy-claim <generationId>").description(
6205
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)"
6206
6649
  ).requiredOption(
@@ -6521,13 +6964,13 @@ function refreshRuntimeVersion(state, binaryOverride) {
6521
6964
  function readExecutorState(start = process.cwd()) {
6522
6965
  const semPath = resolveSemPathForRead(start);
6523
6966
  const sem = semPath ? readSem(semPath) : void 0;
6524
- const path = join11(
6525
- sem ? dirname8(sem.path) : join11(start, ".sechroom"),
6967
+ const path = join12(
6968
+ sem ? dirname8(sem.path) : join12(start, ".sechroom"),
6526
6969
  EXECUTOR_STATE
6527
6970
  );
6528
6971
  if (!existsSync8(path)) return void 0;
6529
6972
  return {
6530
- state: JSON.parse(readFileSync7(path, "utf8")),
6973
+ state: JSON.parse(readFileSync8(path, "utf8")),
6531
6974
  path
6532
6975
  };
6533
6976
  }
@@ -6697,7 +7140,7 @@ function registerChannel(program2) {
6697
7140
  channel.command("install").description(
6698
7141
  "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
6699
7142
  ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
6700
- const path = join12(process.cwd(), ".mcp.json");
7143
+ const path = join13(process.cwd(), ".mcp.json");
6701
7144
  const dryRun = Boolean(opts.dryRun);
6702
7145
  const name = "sechroom-channel";
6703
7146
  const args = ["channel", "mcp"];
@@ -6794,36 +7237,69 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
6794
7237
  }, intervalMilliseconds);
6795
7238
  return () => cancel(timer);
6796
7239
  }
7240
+ var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
7241
+ function classifyLeaseHeartbeatFailure(error) {
7242
+ if (!(error instanceof HttpError)) return "retry";
7243
+ if (error.status === 408 || error.status === 429) return "retry";
7244
+ if (error.status < 400 || error.status >= 500) return "retry";
7245
+ if (error.status === 409 && /\bLease is Released\b/.test(error.body))
7246
+ return "released";
7247
+ return "terminal";
7248
+ }
7249
+ function leaseHeartbeatStoppedLine(leaseId, error) {
7250
+ if (!(error instanceof HttpError))
7251
+ return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
7252
+ return `lease heartbeat stopped for ${leaseId} (${error.status}): ${problemDetail(error.body)}`;
7253
+ }
7254
+ function problemDetail(body) {
7255
+ try {
7256
+ const parsed = JSON.parse(body);
7257
+ const detail = parsed.detail ?? parsed.title;
7258
+ if (typeof detail === "string" && detail.length > 0) return detail;
7259
+ } catch {
7260
+ }
7261
+ return body;
7262
+ }
6797
7263
  function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4, dependencies = {}) {
6798
7264
  const leaseId = claim.lease?.id;
6799
7265
  const claimToken = claim.claimToken;
6800
7266
  if (!leaseId || !claimToken) return void 0;
6801
- const request = dependencies.request ?? api;
7267
+ const request = dependencies.request ?? leaseHeartbeatApi;
6802
7268
  const onError = dependencies.onError ?? ((value) => process.stderr.write(
6803
7269
  err(`channel lease heartbeat failed: ${String(value)}
6804
7270
  `)
6805
7271
  ));
6806
- return startLeaseHeartbeat(
6807
- () => request(
6808
- cfg,
6809
- `/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
6810
- {
6811
- method: "POST",
6812
- body: JSON.stringify({
6813
- claimToken,
6814
- tokenVersion: claim.tokenVersion ?? 1
6815
- })
6816
- }
6817
- ),
6818
- onError,
6819
- intervalMilliseconds,
6820
- {
6821
- setInterval: dependencies.setInterval,
6822
- clearInterval: dependencies.clearInterval
7272
+ let stop;
7273
+ const beat = async () => {
7274
+ try {
7275
+ return await request(
7276
+ cfg,
7277
+ `/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
7278
+ {
7279
+ method: "POST",
7280
+ body: JSON.stringify({
7281
+ claimToken,
7282
+ tokenVersion: claim.tokenVersion ?? 1
7283
+ })
7284
+ }
7285
+ );
7286
+ } catch (error) {
7287
+ const disposition = classifyLeaseHeartbeatFailure(error);
7288
+ if (disposition === "retry") throw error;
7289
+ stop?.();
7290
+ dependencies.onLeaseTerminal?.(leaseId, error);
7291
+ if (disposition === "terminal")
7292
+ onError(leaseHeartbeatStoppedLine(leaseId, error));
7293
+ return void 0;
6823
7294
  }
6824
- );
7295
+ };
7296
+ stop = startLeaseHeartbeat(beat, onError, intervalMilliseconds, {
7297
+ setInterval: dependencies.setInterval,
7298
+ clearInterval: dependencies.clearInterval
7299
+ });
7300
+ return stop;
6825
7301
  }
6826
- function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
7302
+ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
6827
7303
  const stops = /* @__PURE__ */ new Map();
6828
7304
  const intervalMilliseconds = Math.max(
6829
7305
  1e3,
@@ -6836,10 +7312,20 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
6836
7312
  const stop = startChannelTaskLeaseHeartbeat(
6837
7313
  cfg,
6838
7314
  claim,
6839
- intervalMilliseconds
7315
+ intervalMilliseconds,
7316
+ {
7317
+ ...dependencies,
7318
+ // The beat has already cancelled its own timer; drop the dead entry so a
7319
+ // later re-claim of the same lease id can start a fresh beat, and so the
7320
+ // map does not accumulate stopped leases for the life of the channel.
7321
+ onLeaseTerminal: (id) => {
7322
+ stops.delete(id);
7323
+ }
7324
+ }
6840
7325
  );
6841
7326
  if (stop) stops.set(leaseId, stop);
6842
7327
  },
7328
+ activeLeaseIds: () => [...stops.keys()],
6843
7329
  stop: () => {
6844
7330
  for (const stop of stops.values()) stop();
6845
7331
  stops.clear();
@@ -6901,7 +7387,7 @@ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {})
6901
7387
  }
6902
7388
  function readMcpConfig(path) {
6903
7389
  if (!existsSync9(path)) return {};
6904
- const raw = readFileSync8(path, "utf8");
7390
+ const raw = readFileSync9(path, "utf8");
6905
7391
  if (!raw.trim()) return {};
6906
7392
  try {
6907
7393
  return JSON.parse(raw);
@@ -7075,12 +7561,12 @@ Examples:
7075
7561
 
7076
7562
  // src/commands/checkpoint.ts
7077
7563
  import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
7078
- import { dirname as dirname14, join as join16 } from "path";
7564
+ import { dirname as dirname14, join as join17 } from "path";
7079
7565
 
7080
7566
  // src/commands/hook.ts
7081
7567
  import { createHash as createHash4 } from "crypto";
7082
- import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync11, statSync as statSync3, writeFileSync as writeFileSync13 } from "fs";
7083
- import { dirname as dirname13, join as join15 } from "path";
7568
+ import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
7569
+ import { dirname as dirname13, join as join16 } from "path";
7084
7570
 
7085
7571
  // src/commands/lane-commit-hook.ts
7086
7572
  import { execFileSync as execFileSync2 } from "child_process";
@@ -7088,7 +7574,7 @@ import {
7088
7574
  chmodSync,
7089
7575
  existsSync as existsSync10,
7090
7576
  mkdirSync as mkdirSync11,
7091
- readFileSync as readFileSync9,
7577
+ readFileSync as readFileSync10,
7092
7578
  renameSync,
7093
7579
  unlinkSync,
7094
7580
  writeFileSync as writeFileSync10
@@ -7109,7 +7595,7 @@ function resolveCheckoutLane(start) {
7109
7595
  return pin ? applyWorktreeLaneSuffix(pin, start) : void 0;
7110
7596
  }
7111
7597
  function appendLaneTrailer(messagePath, lane) {
7112
- const original = readFileSync9(messagePath, "utf8").replace(/\r\n/g, "\n");
7598
+ const original = readFileSync10(messagePath, "utf8").replace(/\r\n/g, "\n");
7113
7599
  const lines = original.split("\n");
7114
7600
  const scissorsIndex = lines.findIndex(
7115
7601
  (line) => /^#\s*-+\s*>8\s*-+\s*$/.test(line)
@@ -7181,7 +7667,7 @@ function resolveHookPath(root, hookName) {
7181
7667
  function removeLegacyPrepareCommitMsgLeg(root) {
7182
7668
  const path = resolveHookPath(root, LEGACY_HOOK_NAME);
7183
7669
  if (!existsSync10(path)) return;
7184
- const current = readFileSync9(path, "utf8");
7670
+ const current = readFileSync10(path, "utf8");
7185
7671
  const pattern = managedBlockPattern();
7186
7672
  if (!pattern.test(current)) return;
7187
7673
  const next = current.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -7201,7 +7687,7 @@ function installLaneCommitHook(start) {
7201
7687
  }).trim();
7202
7688
  removeLegacyPrepareCommitMsgLeg(root);
7203
7689
  const path = resolveHookPath(root, HOOK_NAME);
7204
- const current = existsSync10(path) ? readFileSync9(path, "utf8") : "";
7690
+ const current = existsSync10(path) ? readFileSync10(path, "utf8") : "";
7205
7691
  let next;
7206
7692
  if (current && !isShellHook(current)) {
7207
7693
  const incumbentPath = nextIncumbentPath(path);
@@ -7224,25 +7710,25 @@ function installLaneCommitHook(start) {
7224
7710
  }
7225
7711
 
7226
7712
  // src/commands/session-context.ts
7227
- import { randomUUID as randomUUID2 } from "crypto";
7228
- import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
7229
- import { dirname as dirname12, join as join14 } from "path";
7713
+ import { randomUUID as randomUUID3 } from "crypto";
7714
+ import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
7715
+ import { dirname as dirname12, join as join15 } from "path";
7230
7716
 
7231
7717
  // src/setup/skill-composition-materialise.ts
7232
- import { createHash as createHash3, randomUUID } from "crypto";
7718
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
7233
7719
  import {
7234
7720
  existsSync as existsSync11,
7235
7721
  mkdirSync as mkdirSync12,
7236
- readdirSync as readdirSync2,
7237
- readFileSync as readFileSync10,
7722
+ readdirSync as readdirSync3,
7723
+ readFileSync as readFileSync11,
7238
7724
  renameSync as renameSync2,
7239
7725
  rmdirSync,
7240
- rmSync as rmSync5,
7241
- statSync as statSync2,
7726
+ rmSync as rmSync6,
7727
+ statSync as statSync3,
7242
7728
  writeFileSync as writeFileSync11
7243
7729
  } from "fs";
7244
- import { dirname as dirname11, isAbsolute as isAbsolute2, join as join13, relative, resolve as resolve5 } from "path";
7245
- var COMPILED_SKILL_LOCK = join13(
7730
+ import { dirname as dirname11, isAbsolute as isAbsolute2, join as join14, relative, resolve as resolve5 } from "path";
7731
+ var COMPILED_SKILL_LOCK = join14(
7246
7732
  ".sechroom",
7247
7733
  "compiled-skill-materialisation.json"
7248
7734
  );
@@ -7307,7 +7793,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
7307
7793
  desired.delete(key);
7308
7794
  }
7309
7795
  }
7310
- const lockPath = join13(cwd, COMPILED_SKILL_LOCK);
7796
+ const lockPath = join14(cwd, COMPILED_SKILL_LOCK);
7311
7797
  const previous = readLock(lockPath, destinations);
7312
7798
  const next = {
7313
7799
  version: 2,
@@ -7318,7 +7804,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
7318
7804
  const path = skillPath(value.skillsRoot, value.skill.name);
7319
7805
  const directory = dirname11(path);
7320
7806
  const prior = previous.entries[key];
7321
- const existing = existsSync11(path) ? readFileSync10(path, "utf8") : void 0;
7807
+ const existing = existsSync11(path) ? readFileSync11(path, "utf8") : void 0;
7322
7808
  const existingHash = existing === void 0 ? void 0 : sha256(existing);
7323
7809
  const owned = prior !== void 0 && prior.skillsRoot === value.skillsRoot && existing !== void 0 && (existingHash === prior.contentHash || existingHash === prior.previousContentHash);
7324
7810
  const recoverableMissing = prior !== void 0 && existing === void 0;
@@ -7366,7 +7852,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
7366
7852
  };
7367
7853
  writeLock(lockPath, next);
7368
7854
  mkdirSync12(directory, { recursive: true });
7369
- if (existingHash !== void 0 && (!existsSync11(path) || sha256(readFileSync10(path, "utf8")) !== existingHash)) {
7855
+ if (existingHash !== void 0 && (!existsSync11(path) || sha256(readFileSync11(path, "utf8")) !== existingHash)) {
7370
7856
  delete next.entries[key];
7371
7857
  writeLock(lockPath, next);
7372
7858
  items.push({
@@ -7449,7 +7935,7 @@ function entryKey(target, name, skillsRoot) {
7449
7935
  return `${target}:${sha256(skillsRoot)}:${name}`;
7450
7936
  }
7451
7937
  function skillPath(skillsRoot, name) {
7452
- return join13(skillsRoot, name, "SKILL.md");
7938
+ return join14(skillsRoot, name, "SKILL.md");
7453
7939
  }
7454
7940
  function withFinalNewline(body) {
7455
7941
  return body.endsWith("\n") ? body : body + "\n";
@@ -7464,7 +7950,7 @@ function readLock(path, destinations) {
7464
7950
  )
7465
7951
  );
7466
7952
  try {
7467
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
7953
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
7468
7954
  if (parsed.version === 2 && parsed.entries && typeof parsed.entries === "object") {
7469
7955
  const entries = {};
7470
7956
  for (const [key, candidate] of Object.entries(parsed.entries)) {
@@ -7495,22 +7981,22 @@ function writeAtomic(path, body) {
7495
7981
  renameSync2(temporary, path);
7496
7982
  }
7497
7983
  function removeOwnedFile(path, acceptableHashes) {
7498
- const quarantine = `${path}.sechroom-retire-${process.pid}-${randomUUID()}`;
7984
+ const quarantine = `${path}.sechroom-retire-${process.pid}-${randomUUID2()}`;
7499
7985
  try {
7500
7986
  renameSync2(path, quarantine);
7501
7987
  } catch (error) {
7502
7988
  if (error.code === "ENOENT") return "missing";
7503
7989
  throw error;
7504
7990
  }
7505
- const quarantinedHash = sha256(readFileSync10(quarantine, "utf8"));
7991
+ const quarantinedHash = sha256(readFileSync11(quarantine, "utf8"));
7506
7992
  if (acceptableHashes.includes(quarantinedHash)) {
7507
- rmSync5(quarantine);
7993
+ rmSync6(quarantine);
7508
7994
  return "removed";
7509
7995
  }
7510
7996
  if (!existsSync11(path)) {
7511
7997
  renameSync2(quarantine, path);
7512
7998
  } else {
7513
- renameSync2(quarantine, `${path}.sechroom-preserved-${randomUUID()}`);
7999
+ renameSync2(quarantine, `${path}.sechroom-preserved-${randomUUID2()}`);
7514
8000
  }
7515
8001
  return "collision";
7516
8002
  }
@@ -7519,7 +8005,7 @@ function syncGitExcludes(cwd, lock) {
7519
8005
  if (!path) return;
7520
8006
  let current = "";
7521
8007
  try {
7522
- current = readFileSync10(path, "utf8");
8008
+ current = readFileSync11(path, "utf8");
7523
8009
  } catch {
7524
8010
  }
7525
8011
  const withoutOwnedBlock = removeOwnedExcludeBlock(current);
@@ -7534,14 +8020,14 @@ function syncGitExcludes(cwd, lock) {
7534
8020
  writeAtomic(path, updated);
7535
8021
  }
7536
8022
  function gitExcludePath(cwd) {
7537
- const dotGit = join13(cwd, ".git");
8023
+ const dotGit = join14(cwd, ".git");
7538
8024
  try {
7539
- if (statSync2(dotGit).isDirectory()) return join13(dotGit, "info", "exclude");
7540
- const pointer = readFileSync10(dotGit, "utf8").trim();
8025
+ if (statSync3(dotGit).isDirectory()) return join14(dotGit, "info", "exclude");
8026
+ const pointer = readFileSync11(dotGit, "utf8").trim();
7541
8027
  if (!pointer.startsWith("gitdir:")) return void 0;
7542
8028
  const raw = pointer.slice("gitdir:".length).trim();
7543
8029
  const gitDir = isAbsolute2(raw) ? raw : resolve5(cwd, raw);
7544
- return join13(gitDir, "info", "exclude");
8030
+ return join14(gitDir, "info", "exclude");
7545
8031
  } catch {
7546
8032
  return void 0;
7547
8033
  }
@@ -7558,13 +8044,13 @@ function removeOwnedExcludeBlock(body) {
7558
8044
  }
7559
8045
  function removeDirectoryIfEmpty(path) {
7560
8046
  try {
7561
- if (readdirSync2(path).length === 0) rmdirSync(path);
8047
+ if (readdirSync3(path).length === 0) rmdirSync(path);
7562
8048
  } catch {
7563
8049
  }
7564
8050
  }
7565
8051
 
7566
8052
  // src/commands/session-context.ts
7567
- var DYNAMIC_AGENT_CONTEXT_FILE = join14(".sechroom", "CLAUDE.md");
8053
+ var DYNAMIC_AGENT_CONTEXT_FILE = join15(".sechroom", "CLAUDE.md");
7568
8054
  function checkoutRoot(start) {
7569
8055
  const semPath = resolveSemPathForRead(start);
7570
8056
  return semPath ? dirname12(dirname12(semPath)) : start;
@@ -7601,7 +8087,7 @@ function renderSessionContext(result, lane) {
7601
8087
  }
7602
8088
  function writeSessionContext(start, lane, result, options = {}) {
7603
8089
  const root = checkoutRoot(start);
7604
- const path = join14(root, DYNAMIC_AGENT_CONTEXT_FILE);
8090
+ const path = join15(root, DYNAMIC_AGENT_CONTEXT_FILE);
7605
8091
  const skills = materialiseCompiledSkillCompositions(
7606
8092
  root,
7607
8093
  result.status === "hold" ? {
@@ -7617,13 +8103,13 @@ function writeSessionContext(start, lane, result, options = {}) {
7617
8103
  );
7618
8104
  const context = renderSessionContext(result, lane);
7619
8105
  mkdirSync13(dirname12(path), { recursive: true });
7620
- const temporaryPath = `${path}.${process.pid}.${randomUUID2()}.tmp`;
8106
+ const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
7621
8107
  try {
7622
8108
  writeFileSync12(temporaryPath, context.endsWith("\n") ? context : `${context}
7623
8109
  `, "utf8");
7624
8110
  renameSync3(temporaryPath, path);
7625
8111
  } catch (error) {
7626
- rmSync6(temporaryPath, { force: true });
8112
+ rmSync7(temporaryPath, { force: true });
7627
8113
  throw error;
7628
8114
  }
7629
8115
  return { status: result.status, path, context, skills };
@@ -7674,11 +8160,12 @@ function resolveLane(flagLane, cwd) {
7674
8160
  if (!base) return void 0;
7675
8161
  return applyWorktreeLaneSuffix(base, start);
7676
8162
  }
7677
- var INTENT_FILE = join15(".sechroom", "continuity.json");
8163
+ var INTENT_FILE = join16(".sechroom", "continuity.json");
8164
+ var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
7678
8165
  function resolveIntentPath(start) {
7679
8166
  let dir = start;
7680
8167
  for (; ; ) {
7681
- const candidate = join15(dir, INTENT_FILE);
8168
+ const candidate = join16(dir, INTENT_FILE);
7682
8169
  if (existsSync12(candidate)) return candidate;
7683
8170
  const parent = dirname13(dir);
7684
8171
  if (parent === dir) return void 0;
@@ -7689,7 +8176,7 @@ function readIntent(start) {
7689
8176
  const path = resolveIntentPath(start);
7690
8177
  if (!path) return void 0;
7691
8178
  try {
7692
- return JSON.parse(readFileSync11(path, "utf8"));
8179
+ return JSON.parse(readFileSync12(path, "utf8"));
7693
8180
  } catch {
7694
8181
  return void 0;
7695
8182
  }
@@ -7699,6 +8186,16 @@ function hasRequiredIntent(i) {
7699
8186
  i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
7700
8187
  );
7701
8188
  }
8189
+ function localDryRunMissingFields(i) {
8190
+ const required = [
8191
+ ["objective", "--objective"],
8192
+ ["state", "--state"],
8193
+ ["lastAction", "--last-action"],
8194
+ ["nextAction", "--next-action"],
8195
+ ["resumeInstruction", "--resume-instruction"]
8196
+ ];
8197
+ return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
8198
+ }
7702
8199
  async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
7703
8200
  const lane = resolveLane(laneFlag, cwd);
7704
8201
  if (!lane) return false;
@@ -7720,7 +8217,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
7720
8217
  openQuestions: intent.questions ?? null,
7721
8218
  surfaceMarkers: intent.surfaceMarkers ?? null,
7722
8219
  relevantArtifactIds: intent.artifacts ?? null,
7723
- confidence: intent.confidence ?? null,
8220
+ // Preserve invalid JSON-string confidence tokens for the server's semantic
8221
+ // validator; never coerce them through Number/NaN/null.
8222
+ confidence: confidenceWireValue(intent.confidence),
7724
8223
  // Frequent triggers (compaction, session-end) land within the FR-051 4h
7725
8224
  // window; Acknowledge lets the checkpoint persist on the lane.
7726
8225
  concurrentSessionPolicy: "Acknowledge"
@@ -7731,14 +8230,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
7731
8230
  }
7732
8231
  function ledgerPath(start) {
7733
8232
  const intent = resolveIntentPath(start);
7734
- const dir = intent ? dirname13(intent) : join15(start, ".sechroom");
7735
- return join15(dir, ".checkpoint-state.json");
8233
+ const dir = intent ? dirname13(intent) : join16(start, ".sechroom");
8234
+ return join16(dir, ".checkpoint-state.json");
7736
8235
  }
7737
8236
  function readLedger(start) {
7738
8237
  try {
7739
8238
  const p = ledgerPath(start);
7740
8239
  if (!existsSync12(p)) return {};
7741
- return JSON.parse(readFileSync11(p, "utf8"));
8240
+ return JSON.parse(readFileSync12(p, "utf8"));
7742
8241
  } catch {
7743
8242
  return {};
7744
8243
  }
@@ -7769,7 +8268,7 @@ function unchangedSinceLastPush(start, intent) {
7769
8268
  const path = resolveIntentPath(start);
7770
8269
  if (path && ledger.lastMtimeMs != null) {
7771
8270
  try {
7772
- if (statSync3(path).mtimeMs <= ledger.lastMtimeMs) return true;
8271
+ if (statSync4(path).mtimeMs <= ledger.lastMtimeMs) return true;
7773
8272
  } catch {
7774
8273
  }
7775
8274
  }
@@ -7781,7 +8280,7 @@ function recordPush(start, intent) {
7781
8280
  const path = resolveIntentPath(start);
7782
8281
  let mtimeMs;
7783
8282
  try {
7784
- if (path) mtimeMs = statSync3(path).mtimeMs;
8283
+ if (path) mtimeMs = statSync4(path).mtimeMs;
7785
8284
  } catch {
7786
8285
  mtimeMs = void 0;
7787
8286
  }
@@ -8015,12 +8514,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
8015
8514
  function registerCheckpoint(program2) {
8016
8515
  program2.command("checkpoint").description(
8017
8516
  "Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
8018
- ).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option("--dry-run", "validate + print the snapshot payload without creating it or writing the file", false).addHelpText(
8517
+ ).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
8518
+ "--dry-run",
8519
+ "run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
8520
+ false
8521
+ ).addHelpText(
8019
8522
  "after",
8020
8523
  `
8021
8524
  File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
8022
8525
  overrides that field. The snapshot is created FIRST (server-validated), then the local file is
8023
8526
  written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
8527
+ --dry-run performs only the local required-field check and prints a payload. Its output is
8528
+ explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
8024
8529
 
8025
8530
  Examples:
8026
8531
  $ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
@@ -8043,7 +8548,9 @@ Examples:
8043
8548
  questions: opts.question ?? base.questions,
8044
8549
  surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
8045
8550
  artifacts: opts.artifact ?? base.artifacts,
8046
- confidence: opts.confidence != null ? Number(opts.confidence) : base.confidence
8551
+ // Keep the raw token until the server's shared validator sees it. In particular,
8552
+ // Number("high") -> NaN -> JSON null would silently discard the operator's input.
8553
+ confidence: opts.confidence != null ? opts.confidence : base.confidence
8047
8554
  };
8048
8555
  const lane = resolveLane(opts.lane, cwd);
8049
8556
  if (!lane) {
@@ -8051,19 +8558,6 @@ Examples:
8051
8558
  "no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
8052
8559
  );
8053
8560
  }
8054
- const required = [
8055
- ["objective", "--objective"],
8056
- ["state", "--state"],
8057
- ["lastAction", "--last-action"],
8058
- ["nextAction", "--next-action"],
8059
- ["resumeInstruction", "--resume-instruction"]
8060
- ];
8061
- const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
8062
- if (missing.length > 0) {
8063
- fail(
8064
- `missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
8065
- );
8066
- }
8067
8561
  const scope = merged.scope ?? "session";
8068
8562
  const body = {
8069
8563
  laneId: lane,
@@ -8077,20 +8571,40 @@ Examples:
8077
8571
  openQuestions: merged.questions ?? null,
8078
8572
  surfaceMarkers: merged.surfaceMarkers ?? null,
8079
8573
  relevantArtifactIds: merged.artifacts ?? null,
8080
- confidence: merged.confidence ?? null,
8574
+ confidence: confidenceWireValue(merged.confidence),
8081
8575
  // Explicit checkpoints are often within the FR-051 4h window; Acknowledge
8082
8576
  // lets one land on the lane (matches `hook pre-compact`).
8083
8577
  concurrentSessionPolicy: "Acknowledge"
8084
8578
  };
8085
8579
  if (opts.dryRun) {
8086
- emit({ dryRun: true, lane, scope, wouldCreate: body }, json);
8580
+ const missing = localDryRunMissingFields(merged);
8581
+ if (missing.length > 0) {
8582
+ fail(
8583
+ `LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
8584
+ );
8585
+ }
8586
+ emit(
8587
+ {
8588
+ dryRun: true,
8589
+ validation: {
8590
+ mode: "local-only",
8591
+ serverValidated: false,
8592
+ checks: ["required-fields"],
8593
+ warning: LOCAL_DRY_RUN_VALIDATION_WARNING
8594
+ },
8595
+ lane,
8596
+ scope,
8597
+ wouldCreate: body
8598
+ },
8599
+ json
8600
+ );
8087
8601
  return;
8088
8602
  }
8089
8603
  const data = await runApi("Creating snapshot", async () => {
8090
8604
  const client = await makeClient(cfg);
8091
8605
  return client.POST("/continuity/snapshots", { body });
8092
8606
  });
8093
- const path = resolveIntentPath(cwd) ?? join16(cwd, INTENT_FILE);
8607
+ const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
8094
8608
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
8095
8609
  mkdirSync15(dirname14(path), { recursive: true });
8096
8610
  writeFileSync14(path, JSON.stringify(fileBody, null, 2) + "\n");
@@ -8107,7 +8621,7 @@ Examples:
8107
8621
  }
8108
8622
 
8109
8623
  // src/commands/close.ts
8110
- import { readFileSync as readFileSync12 } from "fs";
8624
+ import { readFileSync as readFileSync13 } from "fs";
8111
8625
  var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
8112
8626
  function registerClose(program2) {
8113
8627
  program2.command("close").description(
@@ -8148,7 +8662,7 @@ Examples:
8148
8662
  );
8149
8663
  let bodyText;
8150
8664
  try {
8151
- bodyText = opts.file ? readFileSync12(opts.file, "utf8") : readFileSync12(0, "utf8");
8665
+ bodyText = opts.file ? readFileSync13(opts.file, "utf8") : readFileSync13(0, "utf8");
8152
8666
  } catch {
8153
8667
  fail(
8154
8668
  opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
@@ -8320,7 +8834,7 @@ Displaced snapshot recovery surfaces:
8320
8834
  openQuestions: opts.question ?? null,
8321
8835
  surfaceMarkers: opts.surfaceMarker ?? null,
8322
8836
  relevantArtifactIds: opts.artifact ?? null,
8323
- confidence: opts.confidence != null ? Number(opts.confidence) : null
8837
+ confidence: confidenceWireValue(opts.confidence)
8324
8838
  }
8325
8839
  });
8326
8840
  });
@@ -8619,7 +9133,7 @@ Examples:
8619
9133
  );
8620
9134
  });
8621
9135
  workPlan.command("append <decompositionId>").description(
8622
- "Append hand-authored task(s) to a running work plan; they land proposed until ratified (POST /decompositions/{id}/append-tasks)"
9136
+ "Append hand-authored task(s) to a running work plan; they land proposed until ratified. Verification tasks may carry reviewExecution { kind: local-code-review, round, repository, baseCommit, headCommit, preferredInstanceKey?, preferredLaneId? } (POST /decompositions/{id}/append-tasks)"
8623
9137
  ).requiredOption(
8624
9138
  "--file <path>",
8625
9139
  "JSON file containing { tasks, gates? } (the AppendTasksInput shape); use - for stdin"
@@ -9018,14 +9532,34 @@ Examples:
9018
9532
  });
9019
9533
  }
9020
9534
 
9535
+ // src/commands/github.ts
9536
+ function registerGitHub(program2) {
9537
+ const installation = program2.command("github").description("Manage the tenant's GitHub App installation").command("installation").description("Inspect or administer the tenant's GitHub App installation");
9538
+ installation.command("revoke <installationId>").description("Revoke a GitHub App installation (POST /github/app/installations/{installationId}/revoke)").option("--reason <reason>", "Optional reason recorded in the installation audit trail").action(async (installationId, opts, cmd) => {
9539
+ const cfg = resolveConfig(cmd.optsWithGlobals());
9540
+ const data = await runApi("Revoking GitHub App installation", async () => {
9541
+ const client = await makeClient(cfg);
9542
+ return client.POST("/github/app/installations/{installationId}/revoke", {
9543
+ params: { path: { installationId } },
9544
+ body: { reason: opts.reason ?? null }
9545
+ });
9546
+ });
9547
+ emitAction(
9548
+ `revoked GitHub App installation ${style.bold(installationId)}`,
9549
+ data,
9550
+ cmd.optsWithGlobals().json
9551
+ );
9552
+ });
9553
+ }
9554
+
9021
9555
  // src/commands/herdr.ts
9022
- import { readFileSync as readFileSync13 } from "fs";
9556
+ import { readFileSync as readFileSync14 } from "fs";
9023
9557
  import { basename as basename3 } from "path";
9024
9558
 
9025
9559
  // src/herdr/client.ts
9026
9560
  import { createConnection as createConnection2 } from "net";
9027
9561
  import { homedir as homedir5 } from "os";
9028
- import { join as join17 } from "path";
9562
+ import { join as join18 } from "path";
9029
9563
  var DEFAULT_HERDR_SOCKET_RELATIVE = ".config/herdr/herdr.sock";
9030
9564
  var HerdrUnreachableError = class extends Error {
9031
9565
  constructor(socketPath, reason) {
@@ -9050,7 +9584,7 @@ function resolveSocketPath(flag, env = process.env, home = homedir5()) {
9050
9584
  if (fromFlag) return fromFlag;
9051
9585
  const fromEnv = env.HERDR_SOCKET?.trim();
9052
9586
  if (fromEnv) return fromEnv;
9053
- return join17(home, DEFAULT_HERDR_SOCKET_RELATIVE);
9587
+ return join18(home, DEFAULT_HERDR_SOCKET_RELATIVE);
9054
9588
  }
9055
9589
  function expandTarget(target) {
9056
9590
  const trimmed = target.trim();
@@ -9390,7 +9924,7 @@ function parseSource(value, fallback = DEFAULT_READ_SOURCE) {
9390
9924
  }
9391
9925
  return match;
9392
9926
  }
9393
- function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync13(0, "utf8")) {
9927
+ function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync14(0, "utf8")) {
9394
9928
  if (useStdin) {
9395
9929
  if (textArgs.length > 0) {
9396
9930
  throw new Error(
@@ -10130,11 +10664,11 @@ Examples:
10130
10664
  }
10131
10665
 
10132
10666
  // src/commands/memory.ts
10133
- import { readFileSync as readFileSync15 } from "fs";
10667
+ import { readFileSync as readFileSync16 } from "fs";
10134
10668
 
10135
10669
  // src/commands/memory-import.ts
10136
- import { readdirSync as readdirSync3, readFileSync as readFileSync14, realpathSync, statSync as statSync4 } from "fs";
10137
- import { basename as basename4, join as join18, resolve as resolve6 } from "path";
10670
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync, statSync as statSync5 } from "fs";
10671
+ import { basename as basename4, join as join19, resolve as resolve6 } from "path";
10138
10672
  var MARKDOWN_RE = /\.(md|markdown)$/i;
10139
10673
  function isMarkdownPath(path) {
10140
10674
  return MARKDOWN_RE.test(path);
@@ -10161,18 +10695,18 @@ function collectImportFiles(inputs, opts = {}) {
10161
10695
  files.push(path);
10162
10696
  };
10163
10697
  const walk = (dir) => {
10164
- const entries = readdirSync3(dir, { withFileTypes: true }).sort(
10698
+ const entries = readdirSync4(dir, { withFileTypes: true }).sort(
10165
10699
  (a, b) => a.name.localeCompare(b.name)
10166
10700
  );
10167
10701
  for (const entry of entries) {
10168
10702
  if (entry.name.startsWith(".")) continue;
10169
- const child = join18(dir, entry.name);
10703
+ const child = join19(dir, entry.name);
10170
10704
  let isDirectory = entry.isDirectory();
10171
10705
  let isFile = entry.isFile();
10172
10706
  if (entry.isSymbolicLink()) {
10173
10707
  let target;
10174
10708
  try {
10175
- target = statSync4(child);
10709
+ target = statSync5(child);
10176
10710
  } catch {
10177
10711
  skipped.push({ path: child, reason: "broken symlink" });
10178
10712
  continue;
@@ -10210,7 +10744,7 @@ function collectImportFiles(inputs, opts = {}) {
10210
10744
  for (const input of inputs) {
10211
10745
  let isDirectory;
10212
10746
  try {
10213
- isDirectory = statSync4(input).isDirectory();
10747
+ isDirectory = statSync5(input).isDirectory();
10214
10748
  } catch {
10215
10749
  missing.push(input);
10216
10750
  continue;
@@ -10226,7 +10760,7 @@ function buildImportPlan(collected) {
10226
10760
  for (const path of collected.files) {
10227
10761
  let text2;
10228
10762
  try {
10229
- text2 = readFileSync14(path, "utf8");
10763
+ text2 = readFileSync15(path, "utf8");
10230
10764
  } catch (error) {
10231
10765
  throw new Error(
10232
10766
  `couldn't read ${path}: ${error instanceof Error ? error.message : String(error)}`
@@ -10327,7 +10861,7 @@ function resolveCreateBody(textOpt, fileOpt) {
10327
10861
  }
10328
10862
  if (textOpt != null) return { text: textOpt, defaultTitle: null };
10329
10863
  const fromStdin = fileOpt === "-";
10330
- const text2 = fromStdin ? readFileSync15(0, "utf8") : readFileSync15(String(fileOpt), "utf8");
10864
+ const text2 = fromStdin ? readFileSync16(0, "utf8") : readFileSync16(String(fileOpt), "utf8");
10331
10865
  if (text2.trim().length === 0) {
10332
10866
  fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
10333
10867
  }
@@ -10979,16 +11513,16 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
10979
11513
  }
10980
11514
 
10981
11515
  // src/setup/apply.ts
10982
- import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
11516
+ import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
10983
11517
  import {
10984
11518
  chmodSync as chmodSync2,
10985
11519
  copyFileSync,
10986
11520
  existsSync as existsSync13,
10987
11521
  mkdirSync as mkdirSync16,
10988
- readFileSync as readFileSync16,
11522
+ readFileSync as readFileSync17,
10989
11523
  renameSync as renameSync4,
10990
- rmSync as rmSync7,
10991
- statSync as statSync5,
11524
+ rmSync as rmSync8,
11525
+ statSync as statSync6,
10992
11526
  writeFileSync as writeFileSync15
10993
11527
  } from "fs";
10994
11528
  import { dirname as dirname15 } from "path";
@@ -11055,7 +11589,7 @@ function ensureDir2(path) {
11055
11589
  }
11056
11590
  function readOr(path, fallback) {
11057
11591
  try {
11058
- return readFileSync16(path, "utf8");
11592
+ return readFileSync17(path, "utf8");
11059
11593
  } catch {
11060
11594
  return fallback;
11061
11595
  }
@@ -11066,7 +11600,7 @@ function mergeMcpJson(path, snippet, dryRun) {
11066
11600
  let current = {};
11067
11601
  if (existed) {
11068
11602
  try {
11069
- current = JSON.parse(readFileSync16(path, "utf8"));
11603
+ current = JSON.parse(readFileSync17(path, "utf8"));
11070
11604
  } catch {
11071
11605
  return {
11072
11606
  kind: "mcp",
@@ -11436,24 +11970,24 @@ var defaultTomlFileOperations = {
11436
11970
  function writeTomlAtomic(path, content, fileOperations = {}) {
11437
11971
  ensureDir2(path);
11438
11972
  const operations = { ...defaultTomlFileOperations, ...fileOperations };
11439
- const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11973
+ const temporary = `${path}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
11440
11974
  const backup = `${path}.bak`;
11441
- const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11442
- const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
11975
+ const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
11976
+ const mode = existsSync13(path) ? statSync6(path).mode & 4095 : 384;
11443
11977
  try {
11444
11978
  writeFileSync15(temporary, content, { mode });
11445
11979
  chmodSync2(temporary, mode);
11446
- validateToml(readFileSync16(temporary, "utf8"));
11980
+ validateToml(readFileSync17(temporary, "utf8"));
11447
11981
  if (existsSync13(path) && !existsSync13(backup)) {
11448
11982
  operations.copyFileSync(path, backupTemporary);
11449
11983
  chmodSync2(backupTemporary, mode);
11450
- validateToml(readFileSync16(backupTemporary, "utf8"));
11984
+ validateToml(readFileSync17(backupTemporary, "utf8"));
11451
11985
  operations.renameSync(backupTemporary, backup);
11452
11986
  }
11453
11987
  operations.renameSync(temporary, path);
11454
11988
  } finally {
11455
- rmSync7(temporary, { force: true });
11456
- rmSync7(backupTemporary, { force: true });
11989
+ rmSync8(temporary, { force: true });
11990
+ rmSync8(backupTemporary, { force: true });
11457
11991
  }
11458
11992
  }
11459
11993
  function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
@@ -11629,7 +12163,15 @@ async function applyClient(cfg, setup, target, opts) {
11629
12163
  kind: "instruction",
11630
12164
  path: target.instruction.path,
11631
12165
  status: "skipped",
11632
- note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
12166
+ // Name the block even when nothing resolved. Without it the skip reports a
12167
+ // path and no identity, so a consumer cannot tell WHICH managed block went
12168
+ // missing — absent rather than loud, which is the failure this guards.
12169
+ block: "role-template",
12170
+ // GUARD — an unresolved template must NAME what it looked for. A bare
12171
+ // "not found" reads identically whether the bundle is absent or the memo
12172
+ // was retagged out of every candidate family, and the second case is a
12173
+ // silent loss of a template the installation previously had.
12174
+ note: "no role template resolved \u2014 tried " + attemptedTagSets(section).map((set) => `[${set.join(", ")}]`).join(" then ") + " \u2014 install the SEM Starter bundle, or check the template's target tag, then re-run `sechroom setup agent-files`"
11633
12175
  });
11634
12176
  } else {
11635
12177
  const action = applyBlock(
@@ -11734,7 +12276,7 @@ auto-resumes where you left off and checkpoints working state before compacting.
11734
12276
 
11735
12277
  // src/setup/skills-offer.ts
11736
12278
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync16 } from "fs";
11737
- import { join as join19 } from "path";
12279
+ import { join as join20 } from "path";
11738
12280
 
11739
12281
  // src/setup/lane-pin.ts
11740
12282
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -11850,8 +12392,8 @@ Found ${summary} available to you for ${surface}.
11850
12392
  if (skills.length > 0) {
11851
12393
  const written = [];
11852
12394
  for (const s of skills) {
11853
- mkdirSync17(join19(sDir, s.name), { recursive: true });
11854
- writeFileSync16(join19(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
12395
+ mkdirSync17(join20(sDir, s.name), { recursive: true });
12396
+ writeFileSync16(join20(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
11855
12397
  written.push(s.name);
11856
12398
  }
11857
12399
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -11863,7 +12405,7 @@ Found ${summary} available to you for ${surface}.
11863
12405
  const written = [];
11864
12406
  for (const a of agents) {
11865
12407
  const file = `${a.name}.md`;
11866
- writeFileSync16(join19(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
12408
+ writeFileSync16(join20(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
11867
12409
  written.push(file);
11868
12410
  }
11869
12411
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -11976,8 +12518,17 @@ function buildCheckReport(result) {
11976
12518
  stub: 0
11977
12519
  };
11978
12520
  const blocks = [];
12521
+ const unresolved = [];
11979
12522
  for (const { client, actions } of result) {
11980
12523
  for (const action of actions) {
12524
+ if (action.kind === "instruction" && action.status === "skipped" && !action.eval) {
12525
+ unresolved.push({
12526
+ client,
12527
+ path: action.path,
12528
+ block: action.block ?? "unknown",
12529
+ note: action.note
12530
+ });
12531
+ }
11981
12532
  if (!action.eval) continue;
11982
12533
  counts[action.eval]++;
11983
12534
  blocks.push({
@@ -11991,7 +12542,8 @@ function buildCheckReport(result) {
11991
12542
  return {
11992
12543
  eval: counts,
11993
12544
  wouldChange: counts.stale + counts.drift + counts.absent,
11994
- blocks
12545
+ blocks,
12546
+ unresolved
11995
12547
  };
11996
12548
  }
11997
12549
  function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
@@ -12006,7 +12558,7 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
12006
12558
  },
12007
12559
  true
12008
12560
  );
12009
- } else if (report.wouldChange === 0) {
12561
+ } else if (report.wouldChange === 0 && report.unresolved.length === 0) {
12010
12562
  if (report.eval.stub) {
12011
12563
  const files = report.eval.stub === 1 ? "file" : "files";
12012
12564
  process.stdout.write(
@@ -12018,11 +12570,27 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
12018
12570
  }
12019
12571
  } else {
12020
12572
  const bits = [];
12021
- if (report.eval.stale) bits.push(`${report.eval.stale} out of date`);
12022
- if (report.eval.drift) bits.push(`${report.eval.drift} with local edits`);
12023
- if (report.eval.absent) bits.push(`${report.eval.absent} not yet written`);
12573
+ if (report.wouldChange > 0) {
12574
+ const changes = [];
12575
+ if (report.eval.stale) changes.push(`${report.eval.stale} out of date`);
12576
+ if (report.eval.drift)
12577
+ changes.push(`${report.eval.drift} with local edits`);
12578
+ if (report.eval.absent)
12579
+ changes.push(`${report.eval.absent} not yet written`);
12580
+ bits.push(
12581
+ `${report.wouldChange} instruction block(s) would change: ${changes.join(", ")}`
12582
+ );
12583
+ }
12584
+ if (report.unresolved.length > 0) {
12585
+ const details = report.unresolved.map(
12586
+ ({ client, path, block, note }) => `${client} ${block} at ${path}${note ? ` \u2014 ${note}` : ""}`
12587
+ ).join("; ");
12588
+ bits.push(
12589
+ `${report.unresolved.length} instruction template(s) skipped or unresolved: ${details}`
12590
+ );
12591
+ }
12024
12592
  process.stderr.write(
12025
- `\u26A0 ${report.wouldChange} instruction block(s) would change: ${bits.join(", ")}. Run ${style.cyan(refreshCommand)}.
12593
+ `\u26A0 ${bits.join("; ")}. Run ${style.cyan(refreshCommand)}.
12026
12594
  `
12027
12595
  );
12028
12596
  }
@@ -12099,7 +12667,7 @@ function registerInit(program2) {
12099
12667
  false
12100
12668
  ).option(
12101
12669
  "--check",
12102
- "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
12670
+ "report whether agent files would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
12103
12671
  false
12104
12672
  ).addHelpText(
12105
12673
  "after",
@@ -12140,7 +12708,8 @@ Examples:
12140
12708
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
12141
12709
  const targets = clientTargets(process.cwd(), {
12142
12710
  claudeDir: claudeTargets[0]?.dir,
12143
- codexHome: codexHomes[0] ?? null
12711
+ codexHome: codexHomes[0] ?? null,
12712
+ codexScope: scope
12144
12713
  });
12145
12714
  const keys = resolveClientKeys(opts.client);
12146
12715
  const json = g.json;
@@ -12242,7 +12811,7 @@ function registerSetup(program2, deps = {}) {
12242
12811
  false
12243
12812
  ).option(
12244
12813
  "--check",
12245
- "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
12814
+ "report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
12246
12815
  false
12247
12816
  ).addHelpText(
12248
12817
  "after",
@@ -12432,15 +13001,30 @@ Examples:
12432
13001
  "--client <list>",
12433
13002
  `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
12434
13003
  DEFAULT_CLIENT_KEY
13004
+ ).option(
13005
+ "--scope <scope>",
13006
+ "Codex MCP config scope: 'project' (<cwd>/.codex) or 'global' (CODEX_HOME / ~/.codex) \u2014 default project",
13007
+ "project"
12435
13008
  ).option("--dry-run", "print what would be written without writing", false).action(async (slug2, opts, cmd) => {
12436
13009
  const cfg = resolveConfig(cmd.optsWithGlobals());
12437
13010
  const setup = await withSpinner(
12438
13011
  "Fetching setup descriptors",
12439
13012
  () => fetchSetup(cfg, slug2)
12440
13013
  );
12441
- const targets = clientTargets(process.cwd());
13014
+ let scope;
13015
+ try {
13016
+ scope = resolveScope(opts.scope);
13017
+ } catch (error) {
13018
+ return fail(error.message);
13019
+ }
13020
+ const globals = cmd.optsWithGlobals();
13021
+ const codexHome = scope === "global" ? resolveCodexHomes({ override: globals.codexHome, scope })[0] ?? null : null;
13022
+ const targets = clientTargets(process.cwd(), {
13023
+ codexHome,
13024
+ codexScope: scope
13025
+ });
12442
13026
  const keys = resolveClientKeys(opts.client);
12443
- const json = cmd.optsWithGlobals().json;
13027
+ const json = globals.json;
12444
13028
  const result = [];
12445
13029
  for (const key of keys) {
12446
13030
  const target = targets[key];
@@ -12467,12 +13051,12 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
12467
13051
 
12468
13052
  // src/commands/onboard.ts
12469
13053
  import { existsSync as existsSync15 } from "fs";
12470
- import { basename as basename5, join as join21 } from "path";
13054
+ import { basename as basename5, join as join22 } from "path";
12471
13055
 
12472
13056
  // src/commands/fanout.ts
12473
13057
  import { spawnSync } from "child_process";
12474
- import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
12475
- import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
13058
+ import { existsSync as existsSync14, readFileSync as readFileSync18, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
13059
+ import { isAbsolute as isAbsolute3, join as join21, resolve as resolve7 } from "path";
12476
13060
  var ICON = {
12477
13061
  refresh: "\u21BB",
12478
13062
  bind: "+",
@@ -12485,20 +13069,20 @@ function resolveChildDir(path, root) {
12485
13069
  function discoverChildren(root) {
12486
13070
  let names;
12487
13071
  try {
12488
- names = readdirSync4(root);
13072
+ names = readdirSync5(root);
12489
13073
  } catch {
12490
13074
  return [];
12491
13075
  }
12492
13076
  const out = [];
12493
13077
  for (const name of names.sort()) {
12494
13078
  if (name.startsWith(".") || name === "node_modules") continue;
12495
- const dir = join20(root, name);
13079
+ const dir = join21(root, name);
12496
13080
  try {
12497
- if (!statSync6(dir).isDirectory()) continue;
13081
+ if (!statSync7(dir).isDirectory()) continue;
12498
13082
  } catch {
12499
13083
  continue;
12500
13084
  }
12501
- if (existsSync14(join20(dir, ".git")) || committedBindingPath(dir)) out.push(name);
13085
+ if (existsSync14(join21(dir, ".git")) || committedBindingPath(dir)) out.push(name);
12502
13086
  }
12503
13087
  return out;
12504
13088
  }
@@ -12506,7 +13090,7 @@ function readManifest(path) {
12506
13090
  if (!existsSync14(path)) return null;
12507
13091
  let parsed;
12508
13092
  try {
12509
- parsed = JSON.parse(readFileSync17(path, "utf8"));
13093
+ parsed = JSON.parse(readFileSync18(path, "utf8"));
12510
13094
  } catch (err2) {
12511
13095
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
12512
13096
  }
@@ -12974,7 +13558,7 @@ async function planRecurseChild(entry, root, client, opts) {
12974
13558
  reason: "directory does not exist"
12975
13559
  };
12976
13560
  }
12977
- if (existsSync15(join21(dir, ".sechroom.json"))) {
13561
+ if (existsSync15(join22(dir, ".sechroom.json"))) {
12978
13562
  return {
12979
13563
  label: entry.path,
12980
13564
  dir,
@@ -13081,7 +13665,7 @@ This fan-out will pin the same lane in every repo:
13081
13665
  async function runRecurse(cfg, g, opts) {
13082
13666
  const { yes, dryRun, json } = opts;
13083
13667
  const root = process.cwd();
13084
- const manifestPath = join21(root, ".sechroom", "repos.json");
13668
+ const manifestPath = join22(root, ".sechroom", "repos.json");
13085
13669
  const fromManifest = readManifest(manifestPath);
13086
13670
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
13087
13671
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -13185,7 +13769,7 @@ function registerOnboard(program2) {
13185
13769
  false
13186
13770
  ).option(
13187
13771
  "--check",
13188
- "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
13772
+ "report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
13189
13773
  false
13190
13774
  ).option(
13191
13775
  "-y, --yes",
@@ -13330,20 +13914,15 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
13330
13914
  yes,
13331
13915
  process.cwd()
13332
13916
  );
13333
- const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
13334
- if (scope === "project" && requestedKeys.includes("codex") && !json) {
13335
- process.stderr.write(
13336
- `${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
13337
- `
13338
- );
13339
- }
13917
+ const keys = requestedKeys;
13340
13918
  const setup = await withSpinner(
13341
13919
  "Fetching setup descriptors",
13342
13920
  () => fetchSetup(cfg)
13343
13921
  );
13344
13922
  const targets = clientTargets(process.cwd(), {
13345
13923
  claudeDir: claudeTargets[0]?.dir,
13346
- codexHome: codexHomes[0] ?? null
13924
+ codexHome: codexHomes[0] ?? null,
13925
+ codexScope: scope
13347
13926
  });
13348
13927
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
13349
13928
  if (!dryRun && !check) {
@@ -13873,31 +14452,31 @@ Examples:
13873
14452
 
13874
14453
  // src/commands/reset.ts
13875
14454
  import { homedir as homedir6 } from "os";
13876
- import { join as join22 } from "path";
13877
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
14455
+ import { join as join23 } from "path";
14456
+ import { existsSync as existsSync16, readFileSync as readFileSync19, rmSync as rmSync9 } from "fs";
13878
14457
  var SKILLS_LOCK2 = ".sechroom-skills.json";
13879
- var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
13880
- var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
13881
- var localAgentsDir = () => join22(process.cwd(), ".claude", "agents");
13882
- var globalAgentsDir = () => join22(homedir6(), ".claude", "agents");
14458
+ var localSkillsDir = () => join23(process.cwd(), ".claude", "skills");
14459
+ var globalSkillsDir = () => join23(homedir6(), ".claude", "skills");
14460
+ var localAgentsDir = () => join23(process.cwd(), ".claude", "agents");
14461
+ var globalAgentsDir = () => join23(homedir6(), ".claude", "agents");
13883
14462
  function removeMaterialisedSkills(dir) {
13884
14463
  const removed = [];
13885
- const lockPath = join22(dir, SKILLS_LOCK2);
14464
+ const lockPath = join23(dir, SKILLS_LOCK2);
13886
14465
  if (!existsSync16(lockPath)) return removed;
13887
14466
  try {
13888
- const lock = JSON.parse(readFileSync18(lockPath, "utf8"));
14467
+ const lock = JSON.parse(readFileSync19(lockPath, "utf8"));
13889
14468
  for (const entry of Object.values(lock)) {
13890
14469
  for (const name of entry.skills ?? []) {
13891
- const p = join22(dir, name);
14470
+ const p = join23(dir, name);
13892
14471
  if (existsSync16(p)) {
13893
- rmSync8(p, { recursive: true, force: true });
14472
+ rmSync9(p, { recursive: true, force: true });
13894
14473
  removed.push(p);
13895
14474
  }
13896
14475
  }
13897
14476
  }
13898
14477
  } catch {
13899
14478
  }
13900
- rmSync8(lockPath, { force: true });
14479
+ rmSync9(lockPath, { force: true });
13901
14480
  removed.push(lockPath);
13902
14481
  return removed;
13903
14482
  }
@@ -13934,19 +14513,19 @@ function registerReset(program2) {
13934
14513
  }
13935
14514
  }
13936
14515
  const removed = [];
13937
- const stateDir = join22(process.cwd(), ".sechroom");
14516
+ const stateDir = join23(process.cwd(), ".sechroom");
13938
14517
  if (existsSync16(stateDir)) {
13939
- rmSync8(stateDir, { recursive: true, force: true });
14518
+ rmSync9(stateDir, { recursive: true, force: true });
13940
14519
  removed.push(stateDir);
13941
14520
  }
13942
- const legacyCfg = join22(process.cwd(), ".sechroom.json");
14521
+ const legacyCfg = join23(process.cwd(), ".sechroom.json");
13943
14522
  if (existsSync16(legacyCfg)) {
13944
- rmSync8(legacyCfg, { force: true });
14523
+ rmSync9(legacyCfg, { force: true });
13945
14524
  removed.push(legacyCfg);
13946
14525
  }
13947
- const legacySem = join22(process.cwd(), ".sem");
14526
+ const legacySem = join23(process.cwd(), ".sem");
13948
14527
  if (existsSync16(legacySem)) {
13949
- rmSync8(legacySem, { force: true });
14528
+ rmSync9(legacySem, { force: true });
13950
14529
  removed.push(legacySem);
13951
14530
  }
13952
14531
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -13971,8 +14550,8 @@ function registerReset(program2) {
13971
14550
  }
13972
14551
 
13973
14552
  // src/commands/skills.ts
13974
- import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
13975
- import { join as join23 } from "path";
14553
+ import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
14554
+ import { join as join24 } from "path";
13976
14555
  function filenameFromDisposition(header) {
13977
14556
  if (!header) return void 0;
13978
14557
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -13980,11 +14559,11 @@ function filenameFromDisposition(header) {
13980
14559
  }
13981
14560
  function resolveOutputPath(output, serverFilename) {
13982
14561
  const filename = serverFilename || "skills.zip";
13983
- if (!output) return join23(process.cwd(), filename);
13984
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
14562
+ if (!output) return join24(process.cwd(), filename);
14563
+ const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync8(output).isDirectory();
13985
14564
  if (looksLikeDir) {
13986
14565
  mkdirSync18(output, { recursive: true });
13987
- return join23(output, filename);
14566
+ return join24(output, filename);
13988
14567
  }
13989
14568
  return output;
13990
14569
  }
@@ -14030,6 +14609,7 @@ Examples:
14030
14609
  $ sechroom skills install --scope project write them to ./.claude/skills instead
14031
14610
  $ sechroom skills list what's materialised on disk
14032
14611
  $ sechroom skills clean remove the materialised skill files
14612
+ $ sechroom skills clean --prune-orphans also sweep skills orphaned by an earlier rename
14033
14613
  $ sechroom skills preview --workspace wsp_abc render a draft bundle from source (no install)
14034
14614
  $ sechroom skills package my-bundle download the installed bundle's skills as a zip
14035
14615
  $ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
@@ -14043,7 +14623,7 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
14043
14623
  );
14044
14624
  skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
14045
14625
  skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
14046
- skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
14626
+ skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--prune-orphans", "also remove sechroom-installed skill dirs that no lock entry claims (renamed/removed upstream)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
14047
14627
  skills.command("preview").description("Render a workspace's draft bundle from source (no publish/install) and report the components").requiredOption("--workspace <id>", "workspace holding the draft bundle sources (wsp_\u2026)").option("--slug <slug>", "override the derived bundle slug").option("--title <title>", "override the derived bundle title").option("--version <version>", "override the derived bundle version").option("--default-install-parent <path>", "override the derived default install parent").option("--json", "machine output (the full RenderBundlePreviewResponse)").action(async (opts, cmd) => {
14048
14628
  const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
14049
14629
  const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
@@ -14190,8 +14770,8 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
14190
14770
 
14191
14771
  // src/commands/sweep.ts
14192
14772
  import { existsSync as existsSync18 } from "fs";
14193
- import { dirname as dirname16, join as join24, resolve as resolve8 } from "path";
14194
- var DEFAULT_MANIFEST = join24(".sechroom", "repos.json");
14773
+ import { dirname as dirname16, join as join25, resolve as resolve8 } from "path";
14774
+ var DEFAULT_MANIFEST = join25(".sechroom", "repos.json");
14195
14775
  function planEntry(entry, root) {
14196
14776
  const dir = resolveChildDir(entry.path, root);
14197
14777
  if (!existsSync18(dir)) {
@@ -14695,7 +15275,7 @@ async function readStdin4() {
14695
15275
  function resolveVersion() {
14696
15276
  try {
14697
15277
  const pkg = JSON.parse(
14698
- readFileSync19(new URL("../package.json", import.meta.url), "utf8")
15278
+ readFileSync20(new URL("../package.json", import.meta.url), "utf8")
14699
15279
  );
14700
15280
  return pkg.version ?? "0.0.0";
14701
15281
  } catch {
@@ -14859,6 +15439,7 @@ registerWorkPlan(program);
14859
15439
  registerExecutor(program);
14860
15440
  registerClose(program);
14861
15441
  registerFiling(program);
15442
+ registerGitHub(program);
14862
15443
  registerContinuity(program);
14863
15444
  registerCheckpoint(program);
14864
15445
  registerHook(program);