@sechroom/cli 2026.7.8 → 2026.7.9-rc.ed315f06

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 +1090 -165
  2. package/package.json +4 -2
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 readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync13 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -439,6 +439,9 @@ var quiet = false;
439
439
  function setQuiet(q) {
440
440
  quiet = q;
441
441
  }
442
+ function isQuiet() {
443
+ return quiet;
444
+ }
442
445
  function colorOn() {
443
446
  return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
444
447
  }
@@ -615,6 +618,22 @@ function emitAction(summary, data, json) {
615
618
  process.stdout.write(`${ok("\u2713")} ${summary}
616
619
  `);
617
620
  }
621
+ var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
622
+ function isGovernanceQueued(body) {
623
+ return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
624
+ }
625
+ function formatQueuedForApproval(body, json) {
626
+ if (json) return JSON.stringify(body) + "\n";
627
+ const b = body ?? {};
628
+ const detail = typeof b.detail === "string" ? b.detail : void 0;
629
+ const requestId = typeof b.requestId === "string" ? b.requestId : void 0;
630
+ const message = detail ?? "This change requires operator approval before it takes effect.";
631
+ let out = `${warn("\u29D7")} Pending approval \u2014 ${message}
632
+ `;
633
+ if (requestId) out += ` ${style.dim(`request: ${requestId}`)}
634
+ `;
635
+ return out;
636
+ }
618
637
  async function runApi(label, fn) {
619
638
  const s = spinner(label);
620
639
  let res;
@@ -624,6 +643,12 @@ async function runApi(label, fn) {
624
643
  s.fail();
625
644
  fail(err2);
626
645
  }
646
+ const queuedBody = res.data ?? res.error;
647
+ if (res.response?.status === 202 && isGovernanceQueued(queuedBody)) {
648
+ s.stop();
649
+ process.stdout.write(formatQueuedForApproval(queuedBody, isQuiet()));
650
+ process.exit(0);
651
+ }
627
652
  const httpFailed = res.response !== void 0 && !res.response.ok;
628
653
  if (res.error !== void 0 && res.error !== null || httpFailed) {
629
654
  s.fail();
@@ -632,8 +657,32 @@ async function runApi(label, fn) {
632
657
  s.succeed();
633
658
  return res.data;
634
659
  }
660
+ function formatFailureMessage(error) {
661
+ let msg;
662
+ if (error instanceof Error) {
663
+ msg = error.message || error.name;
664
+ if (error.cause instanceof Error && error.cause.message) {
665
+ msg += `: ${error.cause.message}`;
666
+ }
667
+ } else if (typeof error === "object" && error !== null && "title" in error) {
668
+ const problem = error;
669
+ msg = String(problem.title);
670
+ if (problem.errors && typeof problem.errors === "object") {
671
+ const detail = Object.entries(problem.errors).flatMap(
672
+ ([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
673
+ );
674
+ if (detail.length > 0) msg += `
675
+ ${detail.join("\n")}`;
676
+ }
677
+ } else if (typeof error === "object" && error !== null) {
678
+ msg = JSON.stringify(error);
679
+ } else {
680
+ msg = String(error);
681
+ }
682
+ return msg;
683
+ }
635
684
  function fail(error) {
636
- const msg = typeof error === "object" && error !== null && "title" in error ? String(error.title) : typeof error === "object" ? JSON.stringify(error) : String(error);
685
+ const msg = formatFailureMessage(error);
637
686
  process.stderr.write(`error: ${msg}
638
687
  `);
639
688
  process.exit(1);
@@ -1061,14 +1110,23 @@ function bodyOf(row) {
1061
1110
  const m = row?.item ?? row;
1062
1111
  return m?.text ?? m?.Text ?? "";
1063
1112
  }
1113
+ function idOf(row) {
1114
+ const m = row?.item ?? row;
1115
+ return String(m?.id ?? m?.memoryId ?? "unknown");
1116
+ }
1064
1117
  function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
1065
1118
  const out = /* @__PURE__ */ new Map();
1066
1119
  for (const row of rows ?? []) {
1067
1120
  const tags = tagsOf(row);
1068
1121
  if (!tags.includes(roleTag)) continue;
1069
- if (tagValue(tags, "target:") !== surface) continue;
1122
+ if (!tags.includes(`target:${surface}`)) continue;
1070
1123
  const name = tagValue(tags, namePrefix);
1071
1124
  if (!name) continue;
1125
+ if (out.has(name)) {
1126
+ throw new Error(
1127
+ `Ambiguous ${source} ${roleTag} '${name}' for target:${surface}: ${idOf(row)} duplicates another eligible component. Resolve the duplicate before installing.`
1128
+ );
1129
+ }
1072
1130
  out.set(name, { name, body: bodyOf(row), source });
1073
1131
  }
1074
1132
  return out;
@@ -1091,25 +1149,41 @@ function resolveReferences(systemRows, personalRows, surface) {
1091
1149
  }
1092
1150
 
1093
1151
  // src/setup/skill-resolution-io.ts
1094
- var AGENT_TARGET = { "claude-code": "claude-agent" };
1152
+ var AGENT_TARGET = {
1153
+ "claude-code": "claude-agent",
1154
+ "gpt-codex": "gpt-codex-agent"
1155
+ };
1095
1156
  function agentTargetFor(surface) {
1096
1157
  return AGENT_TARGET[surface] ?? `${surface}-agent`;
1097
1158
  }
1098
1159
  async function fetchFeedRows(cfg, workspaceId) {
1099
- try {
1100
- const client = await makeClient(cfg);
1101
- const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
1160
+ const client = await makeClient(cfg);
1161
+ const rows = [];
1162
+ let cursor;
1163
+ do {
1164
+ const { data, error, response } = await client.GET("/workspaces/{workspaceId}/memories/feed", {
1102
1165
  params: {
1103
1166
  path: { workspaceId },
1104
1167
  // cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
1105
1168
  // includeText: the feed omits bodies by default, we need them for SKILL.md.
1106
- query: { limit: 200, cascadeWorkspaces: true, includeText: true }
1169
+ query: {
1170
+ limit: 200,
1171
+ cascadeWorkspaces: true,
1172
+ includeText: true,
1173
+ ...cursor ? { cursor } : {}
1174
+ }
1107
1175
  }
1108
- }).then((r) => r.data).catch(() => void 0);
1109
- return feed?.results ?? feed?.Results ?? [];
1110
- } catch {
1111
- return [];
1112
- }
1176
+ });
1177
+ if (error || !response.ok || !data) {
1178
+ throw new Error(
1179
+ `Could not read operator-skill catalogue from ${workspaceId}: HTTP ${response.status}.`
1180
+ );
1181
+ }
1182
+ const feed = data;
1183
+ rows.push(...feed.results ?? feed.Results ?? []);
1184
+ cursor = feed.nextCursor ?? feed.NextCursor ?? void 0;
1185
+ } while (cursor);
1186
+ return rows;
1113
1187
  }
1114
1188
  async function fetchTemplateRows(cfg, personalWorkspaceId) {
1115
1189
  const [systemRows, personalRows] = await Promise.all([
@@ -1129,7 +1203,10 @@ function resolveReferenceSet(rows, surface) {
1129
1203
  }
1130
1204
 
1131
1205
  // src/setup/materialise.ts
1132
- var CLIENT_SURFACE = "claude-code";
1206
+ var CLIENT_SURFACE = {
1207
+ claude: "claude-code",
1208
+ codex: "gpt-codex"
1209
+ };
1133
1210
  function writeSkills(dir, skills, surface) {
1134
1211
  const written = [];
1135
1212
  for (const s of skills) {
@@ -1140,12 +1217,58 @@ function writeSkills(dir, skills, surface) {
1140
1217
  if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
1141
1218
  return written;
1142
1219
  }
1220
+ function splitAgentFrontmatter(body) {
1221
+ body = body.replaceAll("\r\n", "\n");
1222
+ if (!body.startsWith("---\n")) return { instructions: body };
1223
+ const end = body.indexOf("\n---\n", 4);
1224
+ if (end < 0) return { instructions: body };
1225
+ const frontmatter = body.slice(4, end);
1226
+ const field = (key) => {
1227
+ const line = frontmatter.split("\n").find((candidate) => candidate.startsWith(`${key}:`));
1228
+ return line?.slice(key.length + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
1229
+ };
1230
+ return { name: field("name"), description: field("description"), instructions: body.slice(end + 5).trimStart() };
1231
+ }
1232
+ function validateCodexSkills(skills) {
1233
+ for (const skill of skills) {
1234
+ const parsed = splitAgentFrontmatter(skill.body);
1235
+ if (parsed.name !== skill.name || !parsed.description) {
1236
+ throw new Error(
1237
+ `Codex skill '${skill.name}' must begin with YAML frontmatter containing matching name and a description.`
1238
+ );
1239
+ }
1240
+ }
1241
+ }
1242
+ var WORKER_NAMES = ["substrate-drafter", "substrate-miner", "substrate-verifier"];
1243
+ function validateCodexWorkerDependencies(skills, agents) {
1244
+ const available = new Set(agents.map((agent) => agent.name));
1245
+ const missing = WORKER_NAMES.filter(
1246
+ (worker) => skills.some((skill) => skill.body.includes(worker)) && !available.has(worker)
1247
+ );
1248
+ if (missing.length) {
1249
+ throw new Error(`Codex skills reference missing agent template(s): ${missing.join(", ")}.`);
1250
+ }
1251
+ }
1252
+ function codexAgentToml(agent) {
1253
+ const parsed = splitAgentFrontmatter(agent.body);
1254
+ if (!parsed.description) {
1255
+ throw new Error(`Codex agent '${agent.name}' requires a description in its leading YAML frontmatter.`);
1256
+ }
1257
+ return [
1258
+ `name = ${JSON.stringify(parsed.name || agent.name)}`,
1259
+ `description = ${JSON.stringify(parsed.description)}`,
1260
+ `developer_instructions = ${JSON.stringify(parsed.instructions.trimEnd())}`,
1261
+ ""
1262
+ ].join("\n");
1263
+ }
1143
1264
  function writeAgents(dir, agents, surface) {
1144
1265
  if (agents.length) mkdirSync3(dir, { recursive: true });
1145
1266
  const written = [];
1146
1267
  for (const a of agents) {
1147
- const file = `${a.name}.md`;
1148
- writeFileSync3(join4(dir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
1268
+ const codex = surface === CLIENT_SURFACE.codex;
1269
+ const file = `${a.name}.${codex ? "toml" : "md"}`;
1270
+ const body = codex ? codexAgentToml(a) : a.body.endsWith("\n") ? a.body : a.body + "\n";
1271
+ writeFileSync3(join4(dir, file), body);
1149
1272
  written.push(file);
1150
1273
  }
1151
1274
  if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -1162,11 +1285,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
1162
1285
  }
1163
1286
  return refs.map((r) => r.name);
1164
1287
  }
1165
- var SKILL_SPEC = { kind: "skill", dir: skillsDir, resolve: resolveSkillSet, write: writeSkills };
1166
- var AGENT_SPEC = { kind: "agent", dir: agentsDir, resolve: resolveAgentSet, write: writeAgents };
1288
+ var SKILL_SPEC = {
1289
+ kind: "skill",
1290
+ dir: skillsDir,
1291
+ resolve: resolveSkillSet,
1292
+ write: writeSkills,
1293
+ supportsCodex: true
1294
+ };
1295
+ var AGENT_SPEC = {
1296
+ kind: "agent",
1297
+ dir: agentsDir,
1298
+ resolve: resolveAgentSet,
1299
+ write: writeAgents,
1300
+ supportsCodex: true
1301
+ };
1167
1302
  function scopeOf(opts) {
1168
1303
  return opts.local ? "project" : resolveScope(opts.scope);
1169
1304
  }
1305
+ function parseClient(value) {
1306
+ if (value == null) return void 0;
1307
+ if (value === "claude" || value === "codex" || value === "all") return value;
1308
+ throw new Error(`--client must be 'claude', 'codex', or 'all' (got '${value}')`);
1309
+ }
1310
+ function clientSelection(spec, g, raw) {
1311
+ if (!spec.supportsCodex) return "claude";
1312
+ const explicit = parseClient(raw);
1313
+ if (explicit) return explicit;
1314
+ const claudeConfigured = Boolean(g.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
1315
+ const codexConfigured = Boolean(g.codexHome || process.env.CODEX_HOME);
1316
+ if (claudeConfigured || codexConfigured) {
1317
+ if (claudeConfigured && codexConfigured) return "all";
1318
+ return codexConfigured ? "codex" : "claude";
1319
+ }
1320
+ const claudeDetected = resolveClaudeTargets({})[0]?.dir;
1321
+ const codexDetected = resolveCodexHomes({})[0];
1322
+ const hasClaude = Boolean(claudeDetected && existsSync3(claudeDetected));
1323
+ const hasCodex = Boolean(codexDetected && existsSync3(codexDetected));
1324
+ if (hasClaude && hasCodex) return "all";
1325
+ if (hasCodex) return "codex";
1326
+ return "claude";
1327
+ }
1328
+ function targetsFor(spec, g, scope, selection) {
1329
+ const clients = selection === "all" ? ["claude", "codex"] : [selection];
1330
+ if (scope === "project" && clients.includes("codex")) {
1331
+ throw new Error("Codex skills have no supported project scope; use --scope global or --client claude.");
1332
+ }
1333
+ const targets = [];
1334
+ for (const client of clients) {
1335
+ if (client === "claude") {
1336
+ targets.push(...resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() }).map((target) => ({
1337
+ client,
1338
+ surface: CLIENT_SURFACE[client],
1339
+ dir: spec.dir(target.dir),
1340
+ label: target.label
1341
+ })));
1342
+ continue;
1343
+ }
1344
+ targets.push(...resolveCodexHomes({ override: g.codexHome, scope }).map((home) => ({
1345
+ client,
1346
+ surface: CLIENT_SURFACE[client],
1347
+ dir: spec.dir(home),
1348
+ label: home
1349
+ })));
1350
+ }
1351
+ return targets;
1352
+ }
1170
1353
  async function runInstall(spec, cmd, opts) {
1171
1354
  const g = cmd.optsWithGlobals();
1172
1355
  let scope;
@@ -1178,31 +1361,52 @@ async function runInstall(spec, cmd, opts) {
1178
1361
  const cfg = resolveConfig(g);
1179
1362
  const dryRun = Boolean(opts.dryRun);
1180
1363
  const json = Boolean(g.json || opts.json);
1181
- const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
1364
+ let selection;
1365
+ let targets;
1366
+ try {
1367
+ selection = clientSelection(spec, g, opts.client);
1368
+ targets = targetsFor(spec, g, scope, selection);
1369
+ } catch (err2) {
1370
+ return fail(err2.message);
1371
+ }
1182
1372
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
1183
1373
  const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
1184
- const items = spec.resolve(rows, CLIENT_SURFACE);
1185
- const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
1186
1374
  const results = targets.map((t) => {
1187
- const dir = spec.dir(t.dir);
1188
- const written = dryRun ? items.map((i) => i.name) : spec.write(dir, items, CLIENT_SURFACE);
1189
- const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(dir, items, refs);
1190
- return { dir, label: t.label, written, refsWritten };
1375
+ const items = spec.resolve(rows, t.surface);
1376
+ if (t.client === "codex" && spec.kind === "skill") {
1377
+ validateCodexSkills(items);
1378
+ validateCodexWorkerDependencies(items, resolveAgentSet(rows, t.surface));
1379
+ }
1380
+ const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
1381
+ const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
1382
+ const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
1383
+ return {
1384
+ client: t.client,
1385
+ surface: t.surface,
1386
+ dir: t.dir,
1387
+ label: t.label,
1388
+ available: items.length,
1389
+ references: refs.length,
1390
+ items: items.map(({ name, source }) => ({ name, source })),
1391
+ referenceItems: refs.map(({ name, source }) => ({ name, source })),
1392
+ written,
1393
+ refsWritten
1394
+ };
1191
1395
  });
1192
- if (json) return emit({ kind: spec.kind, dryRun, available: items.length, references: refs.length, targets: results }, true);
1193
- if (items.length === 0) {
1396
+ if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
1397
+ if (results.every((r) => r.available === 0)) {
1194
1398
  console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
1195
1399
  return;
1196
1400
  }
1197
1401
  for (const r of results) {
1198
1402
  console.log(
1199
- `${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
1403
+ `${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) for ${r.client} ${style.dim("\u2192")} ${r.dir}`
1200
1404
  );
1201
1405
  if (r.refsWritten.length)
1202
1406
  console.log(
1203
1407
  `${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
1204
1408
  );
1205
- if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
1409
+ if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
1206
1410
  }
1207
1411
  }
1208
1412
  function runList(spec, cmd, opts) {
@@ -1214,16 +1418,22 @@ function runList(spec, cmd, opts) {
1214
1418
  return fail(err2.message);
1215
1419
  }
1216
1420
  const json = Boolean(g.json || opts.json);
1217
- const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
1421
+ let selection;
1422
+ let targets;
1423
+ try {
1424
+ selection = clientSelection(spec, g, opts.client);
1425
+ targets = targetsFor(spec, g, scope, selection);
1426
+ } catch (err2) {
1427
+ return fail(err2.message);
1428
+ }
1218
1429
  const out = targets.map((t) => {
1219
- const dir = spec.dir(t.dir);
1220
- const lock = readSkillsLock(dir);
1430
+ const lock = readSkillsLock(t.dir);
1221
1431
  const entries = Object.entries(lock).flatMap(
1222
- ([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(dir, name)) }))
1432
+ ([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(t.dir, name)) }))
1223
1433
  );
1224
- return { dir, label: t.label, entries };
1434
+ return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
1225
1435
  });
1226
- if (json) return emit({ kind: spec.kind, targets: out }, true);
1436
+ if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
1227
1437
  let any = false;
1228
1438
  for (const t of out) {
1229
1439
  if (t.entries.length === 0) continue;
@@ -1246,33 +1456,39 @@ function runClean(spec, cmd, opts, slugArg) {
1246
1456
  return fail(err2.message);
1247
1457
  }
1248
1458
  const json = Boolean(g.json || opts.json);
1249
- const targets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
1459
+ let selection;
1460
+ let targets;
1461
+ try {
1462
+ selection = clientSelection(spec, g, opts.client);
1463
+ targets = targetsFor(spec, g, scope, selection);
1464
+ } catch (err2) {
1465
+ return fail(err2.message);
1466
+ }
1250
1467
  const cleaned = [];
1251
1468
  const missing = [];
1252
1469
  for (const t of targets) {
1253
- const dir = spec.dir(t.dir);
1254
- const lock = readSkillsLock(dir);
1470
+ const lock = readSkillsLock(t.dir);
1255
1471
  const entry = lock[slug];
1256
1472
  if (!entry) {
1257
- missing.push(join4(dir, SKILLS_LOCK));
1473
+ missing.push(join4(t.dir, SKILLS_LOCK));
1258
1474
  continue;
1259
1475
  }
1260
1476
  const removed = [];
1261
1477
  for (const name of entry.skills) {
1262
- const p = join4(dir, name);
1478
+ const p = join4(t.dir, name);
1263
1479
  if (existsSync3(p)) {
1264
1480
  rmSync2(p, { recursive: true, force: true });
1265
1481
  removed.push(name);
1266
1482
  }
1267
1483
  }
1268
1484
  delete lock[slug];
1269
- writeSkillsLock(dir, lock);
1270
- cleaned.push({ dir, removed });
1485
+ writeSkillsLock(t.dir, lock);
1486
+ cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
1271
1487
  }
1272
1488
  if (cleaned.length === 0) {
1273
1489
  return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
1274
1490
  }
1275
- if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
1491
+ if (json) return emit({ kind: spec.kind, client: selection, slug, cleaned, missing }, true);
1276
1492
  for (const c of cleaned) {
1277
1493
  console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
1278
1494
  }
@@ -1286,17 +1502,19 @@ function registerAgents(program2) {
1286
1502
  `
1287
1503
  Examples:
1288
1504
  $ sechroom agents install materialise your installed agents to ~/.claude/agents
1505
+ $ sechroom agents install --client codex materialise native TOML agents to ~/.codex/agents
1289
1506
  $ sechroom agents install --scope project write them to ./.claude/agents instead
1290
1507
  $ sechroom agents install --claude-config-dir ~/.claude-work target another instance
1291
1508
  $ sechroom agents list what's materialised on disk
1292
1509
  $ sechroom agents clean remove the materialised agent files
1293
1510
 
1294
- Agents are resolved from the agent target (target:claude-agent), the dispatchable
1295
- workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
1511
+ Agents are resolved from the client's agent target (target:claude-agent or
1512
+ target:gpt-codex-agent), the dispatchable workers your loop skills call
1513
+ (e.g. find-prior-art \u2192 substrate-miner).`
1296
1514
  );
1297
- 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("--json", "machine output").action((opts, cmd) => runInstall(AGENT_SPEC, cmd, opts));
1298
- 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").action((opts, cmd) => runList(AGENT_SPEC, cmd, opts));
1299
- 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").action((slugArg, opts, cmd) => runClean(AGENT_SPEC, cmd, opts, slugArg));
1515
+ 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));
1516
+ 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));
1517
+ 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));
1300
1518
  }
1301
1519
 
1302
1520
  // src/commands/channel.ts
@@ -1388,15 +1606,24 @@ var CLAUDE_HOOK_COMMANDS = {
1388
1606
  SessionStart: "sechroom hook session-start",
1389
1607
  PreCompact: "sechroom hook pre-compact",
1390
1608
  SessionEnd: "sechroom hook session-end",
1391
- // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1392
- // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1393
- // for every Claude install. Claude-only (it parses a Claude Code transcript).
1394
- Stop: "sechroom telemetry hook"
1609
+ // WLP telemetry tap (D-WLP-10 + FR-352 Tier 1) — per-turn executor self-report. The one
1610
+ // `telemetry hook` verb dispatches on hook_event_name: Stop/SubagentStop parsed (token/context) +
1611
+ // terminal (turn end), Notification/PermissionDenied → approval. No-op (exit 0) unless this checkout
1612
+ // is bound via `sechroom telemetry bind`, so it's safe to wire for every Claude install; an event a
1613
+ // given Claude Code version doesn't know is inert (never fires). Claude-only.
1614
+ Stop: "sechroom telemetry hook",
1615
+ SubagentStop: "sechroom telemetry hook",
1616
+ Notification: "sechroom telemetry hook",
1617
+ PermissionDenied: "sechroom telemetry hook"
1395
1618
  };
1396
1619
  var CODEX_HOOK_COMMANDS = {
1397
1620
  SessionStart: "sechroom hook session-start",
1621
+ PreCompact: "sechroom hook pre-compact",
1398
1622
  Stop: "sechroom hook session-end --debounce-minutes 10"
1399
1623
  };
1624
+ function hookCommandsForSurface(surface) {
1625
+ return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
1626
+ }
1400
1627
  function hasHookCommand(config2, event, command) {
1401
1628
  const groups = config2.hooks?.[event] ?? [];
1402
1629
  return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
@@ -1432,6 +1659,12 @@ function installHooksJson(path, commands, dryRun) {
1432
1659
  function installClaudeCommands(claudeDir, commands, dryRun) {
1433
1660
  return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
1434
1661
  }
1662
+ function installCodexCommands(codexHome, commands, dryRun) {
1663
+ return [
1664
+ installHooksJson(join6(codexHome, "hooks.json"), commands, dryRun),
1665
+ installCodexFeatureFlag(join6(codexHome, "config.toml"), dryRun)
1666
+ ];
1667
+ }
1435
1668
  function ensureCodexFeaturesHooks(content) {
1436
1669
  const lines = content.split("\n");
1437
1670
  const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
@@ -1534,7 +1767,10 @@ function registerChannel(program2) {
1534
1767
  "--tag <tag...>",
1535
1768
  "Tag(s) the event must carry to match (repeatable). Default targets WLP task dispatches.",
1536
1769
  ["kind:task"]
1537
- ).option("--workspace <wsp...>", "Restrict to these workspace id(s)");
1770
+ ).option("--workspace <wsp...>", "Restrict to these workspace id(s)").option(
1771
+ "--executor-instance <id>",
1772
+ "Join the exact executor-instance SignalR group"
1773
+ );
1538
1774
  withFilterOpts(
1539
1775
  channel.command("connect").description(
1540
1776
  "Register a SignalR subscription and stream matched events to stdout"
@@ -1552,7 +1788,7 @@ function registerChannel(program2) {
1552
1788
  (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
1553
1789
  )
1554
1790
  );
1555
- const conn = await openConnection(cfg, deliver);
1791
+ const conn = await openConnection(cfg, deliver, opts.executorInstance);
1556
1792
  await reconcile(cfg, filter, deliver);
1557
1793
  if (json) {
1558
1794
  emit(
@@ -1601,7 +1837,7 @@ function registerChannel(program2) {
1601
1837
  `))
1602
1838
  );
1603
1839
  });
1604
- const conn = await openConnection(cfg, deliver);
1840
+ const conn = await openConnection(cfg, deliver, opts.executorInstance);
1605
1841
  await reconcile(cfg, filter, deliver);
1606
1842
  process.stderr.write(
1607
1843
  style.dim(
@@ -1712,8 +1948,9 @@ async function ensureSubscription(cfg, name, filter) {
1712
1948
  );
1713
1949
  return await resp.json();
1714
1950
  }
1715
- async function openConnection(cfg, onEvent) {
1716
- const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}`, {
1951
+ async function openConnection(cfg, onEvent, executorInstanceId) {
1952
+ const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
1953
+ const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
1717
1954
  transport: HttpTransportType.LongPolling,
1718
1955
  accessTokenFactory: () => requireToken(cfg)
1719
1956
  }).withAutomaticReconnect().build();
@@ -2416,6 +2653,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
2416
2653
  const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
2417
2654
  process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
2418
2655
  `);
2656
+ if (dryRun) {
2657
+ for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
2658
+ process.stdout.write(` ${event}: ${command}
2659
+ `);
2660
+ }
2661
+ }
2419
2662
  for (const r of surfaceResults) {
2420
2663
  results.push(r);
2421
2664
  process.stdout.write(describe(r, dryRun) + "\n");
@@ -2533,6 +2776,178 @@ Examples:
2533
2776
  });
2534
2777
  }
2535
2778
 
2779
+ // src/commands/close.ts
2780
+ import { readFileSync as readFileSync7 } from "fs";
2781
+ var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
2782
+ function registerClose(program2) {
2783
+ program2.command("close").description(
2784
+ "Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
2785
+ ).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
2786
+ "--workspace <wsp>",
2787
+ "Workspace that owns the closeout memo"
2788
+ ).requiredOption("--title <title>", "Closeout title").option(
2789
+ "--file <path>",
2790
+ "Read the closeout body from a file (default: stdin)"
2791
+ ).option(
2792
+ "--decomposition <id>",
2793
+ "Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
2794
+ ).option(
2795
+ "--no-status-flip",
2796
+ "Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
2797
+ ).option(
2798
+ "--to-version <n>",
2799
+ "Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
2800
+ ).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
2801
+ "after",
2802
+ `
2803
+ Examples:
2804
+ # Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
2805
+ $ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
2806
+ --verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
2807
+
2808
+ # Bare task:
2809
+ $ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
2810
+ --title "done" --file ./closeout.md`
2811
+ ).action(async (opts, cmd) => {
2812
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2813
+ const json = cmd.optsWithGlobals().json;
2814
+ const verdict = String(opts.verdict);
2815
+ if (!VERDICTS.includes(verdict))
2816
+ fail(
2817
+ `--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
2818
+ );
2819
+ let bodyText;
2820
+ try {
2821
+ bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
2822
+ } catch {
2823
+ fail(
2824
+ opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
2825
+ );
2826
+ }
2827
+ if (!bodyText.trim())
2828
+ fail(
2829
+ "closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
2830
+ );
2831
+ const client = await makeClient(cfg);
2832
+ let matchTags;
2833
+ let dispatchedVersion;
2834
+ if (opts.decomposition) {
2835
+ const run = await runApi(
2836
+ "Reading run contract",
2837
+ async () => client.GET("/decompositions/{id}/run", {
2838
+ params: { path: { id: opts.decomposition } }
2839
+ })
2840
+ );
2841
+ const await_ = run.awaitingCompletion;
2842
+ if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
2843
+ fail(
2844
+ `decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
2845
+ );
2846
+ if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
2847
+ fail(
2848
+ `run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
2849
+ );
2850
+ matchTags = await_.matchTags;
2851
+ if (typeof await_.dispatchedTaskVersion === "number")
2852
+ dispatchedVersion = await_.dispatchedTaskVersion;
2853
+ } else {
2854
+ matchTags = [`wlp-task:${opts.task}`];
2855
+ }
2856
+ const tags = [
2857
+ .../* @__PURE__ */ new Set([
2858
+ ...matchTags,
2859
+ `wlp-task:${opts.task}`,
2860
+ `verdict:${verdict}`
2861
+ ])
2862
+ ];
2863
+ const closeout = await runApi(
2864
+ "Creating closeout",
2865
+ async () => client.POST("/memories", {
2866
+ body: {
2867
+ text: bodyText,
2868
+ type: "reference",
2869
+ content: "{}",
2870
+ confidence: 1,
2871
+ source: opts.source,
2872
+ archetype: "Document",
2873
+ title: opts.title,
2874
+ tags,
2875
+ owner: { type: "Workspace", id: opts.workspace }
2876
+ }
2877
+ })
2878
+ );
2879
+ let toVersion;
2880
+ if (opts.toVersion !== void 0) {
2881
+ toVersion = Number(opts.toVersion);
2882
+ if (!Number.isInteger(toVersion) || toVersion < 1)
2883
+ fail(
2884
+ `--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
2885
+ );
2886
+ } else if (dispatchedVersion !== void 0) {
2887
+ toVersion = dispatchedVersion;
2888
+ } else {
2889
+ const task = await runApi(
2890
+ "Reading task version",
2891
+ async () => client.GET("/memories/{memoryId}", {
2892
+ params: { path: { memoryId: opts.task } }
2893
+ })
2894
+ );
2895
+ const v = task?.item?.currentVersion ?? task?.currentVersion;
2896
+ if (typeof v !== "number" || v < 1)
2897
+ fail(
2898
+ `could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
2899
+ );
2900
+ toVersion = v;
2901
+ }
2902
+ const edge = await runApi(
2903
+ "Wiring Reference edge",
2904
+ async () => client.POST("/memories/{memoryId}/relationships", {
2905
+ params: { path: { memoryId: closeout.id } },
2906
+ body: { toMemoryId: opts.task, type: "Reference", toVersion }
2907
+ })
2908
+ );
2909
+ let taskStatus;
2910
+ if (opts.statusFlip !== false && !opts.decomposition) {
2911
+ const current = await runApi(
2912
+ "Reading task tags",
2913
+ async () => client.GET("/memories/{memoryId}", {
2914
+ params: { path: { memoryId: opts.task } }
2915
+ })
2916
+ );
2917
+ const currentTags = current?.item?.tags ?? current?.tags ?? [];
2918
+ const target = verdict === "blocked" ? "status:blocked" : "status:done";
2919
+ const nextTags = [
2920
+ ...new Set(currentTags.filter((t) => !t.startsWith("status:")))
2921
+ ].concat(target);
2922
+ await runApi(
2923
+ "Flipping task status",
2924
+ async () => client.PATCH("/memories/{memoryId}/metadata", {
2925
+ params: { path: { memoryId: opts.task } },
2926
+ body: {
2927
+ memoryId: opts.task,
2928
+ source: opts.source,
2929
+ tags: nextTags
2930
+ }
2931
+ })
2932
+ );
2933
+ taskStatus = target.slice("status:".length);
2934
+ }
2935
+ const view = resolveViewUrl(cfg.baseUrl, closeout.url);
2936
+ const result = {
2937
+ closeoutId: closeout.id,
2938
+ edgeId: edge.id,
2939
+ taskStatus: taskStatus ?? null,
2940
+ verdict,
2941
+ tags
2942
+ };
2943
+ emitAction(
2944
+ `closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
2945
+ result,
2946
+ json
2947
+ );
2948
+ });
2949
+ }
2950
+
2536
2951
  // src/commands/continuity.ts
2537
2952
  function registerContinuity(program2) {
2538
2953
  const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
@@ -2693,6 +3108,442 @@ Examples:
2693
3108
  });
2694
3109
  }
2695
3110
 
3111
+ // src/commands/decomposition.ts
3112
+ function registerDecomposition(program2) {
3113
+ const decomposition = program2.command("decomposition").description(
3114
+ "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
3115
+ );
3116
+ decomposition.addHelpText(
3117
+ "after",
3118
+ `
3119
+ Examples:
3120
+ $ sechroom decomposition decompose mem_XXXX
3121
+ $ sechroom decomposition execute sug_XXXX
3122
+ $ sechroom decomposition publish-run sug_XXXX
3123
+ $ sechroom decomposition accept sug_XXXX
3124
+ $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
3125
+ );
3126
+ decomposition.command("decompose <briefId>").description(
3127
+ "Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
3128
+ ).action(async (briefId, _opts, cmd) => {
3129
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3130
+ const data = await runApi("Queueing decomposition", async () => {
3131
+ const client = await makeClient(cfg);
3132
+ return client.POST("/work-briefs/{id}/decompose", {
3133
+ params: { path: { id: briefId } },
3134
+ body: { id: briefId }
3135
+ });
3136
+ });
3137
+ emitAction(
3138
+ `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
3139
+ data,
3140
+ cmd.optsWithGlobals().json
3141
+ );
3142
+ });
3143
+ decomposition.command("execute <decompositionId>").description(
3144
+ "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
3145
+ ).action(async (decompositionId, _opts, cmd) => {
3146
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3147
+ const data = await runApi("Executing decomposition", async () => {
3148
+ const client = await makeClient(cfg);
3149
+ return client.POST("/decompositions/{id}/execute", {
3150
+ params: { path: { id: decompositionId } },
3151
+ body: {}
3152
+ });
3153
+ });
3154
+ emitAction(
3155
+ `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3156
+ data,
3157
+ cmd.optsWithGlobals().json
3158
+ );
3159
+ });
3160
+ decomposition.command("publish-run <decompositionId>").description(
3161
+ "Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
3162
+ ).action(async (decompositionId, _opts, cmd) => {
3163
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3164
+ const data = await runApi("Publishing context pack", async () => {
3165
+ const client = await makeClient(cfg);
3166
+ return client.POST("/decompositions/{id}/publish-run", {
3167
+ params: { path: { id: decompositionId } },
3168
+ body: {}
3169
+ });
3170
+ });
3171
+ emitAction(
3172
+ `published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3173
+ data,
3174
+ cmd.optsWithGlobals().json
3175
+ );
3176
+ });
3177
+ decomposition.command("accept <decompositionId>").description(
3178
+ "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
3179
+ ).action(async (decompositionId, _opts, cmd) => {
3180
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3181
+ const data = await runApi("Accepting decomposition", async () => {
3182
+ const client = await makeClient(cfg);
3183
+ return client.POST("/decompositions/{id}/accept", {
3184
+ params: { path: { id: decompositionId } },
3185
+ body: {}
3186
+ });
3187
+ });
3188
+ emitAction(
3189
+ `accepted decomposition ${style.bold(decompositionId)}`,
3190
+ data,
3191
+ cmd.optsWithGlobals().json
3192
+ );
3193
+ });
3194
+ decomposition.command("reject <decompositionId>").description(
3195
+ "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
3196
+ ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
3197
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3198
+ const data = await runApi("Rejecting decomposition", async () => {
3199
+ const client = await makeClient(cfg);
3200
+ return client.POST("/decompositions/{id}/reject", {
3201
+ params: { path: { id: decompositionId } },
3202
+ body: { reasonText: opts.reason ?? null }
3203
+ });
3204
+ });
3205
+ emitAction(
3206
+ `rejected decomposition ${style.bold(decompositionId)}`,
3207
+ data,
3208
+ cmd.optsWithGlobals().json
3209
+ );
3210
+ });
3211
+ }
3212
+
3213
+ // src/commands/executor.ts
3214
+ import { dirname as dirname8, join as join11 } from "path";
3215
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
3216
+ var EXECUTOR_STATE = "executor.json";
3217
+ var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
3218
+ var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
3219
+ var CLAUDE_EXECUTOR_HOOKS = {
3220
+ SessionStart: EXECUTOR_PULSE_COMMAND,
3221
+ UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
3222
+ PreToolUse: EXECUTOR_PULSE_COMMAND,
3223
+ Stop: EXECUTOR_PULSE_COMMAND,
3224
+ SessionEnd: EXECUTOR_STOP_COMMAND
3225
+ };
3226
+ var CODEX_EXECUTOR_HOOKS = {
3227
+ SessionStart: EXECUTOR_PULSE_COMMAND,
3228
+ UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
3229
+ PreToolUse: EXECUTOR_PULSE_COMMAND,
3230
+ Stop: EXECUTOR_PULSE_COMMAND
3231
+ };
3232
+ function registerExecutor(program2) {
3233
+ const executor = program2.command("executor").description(
3234
+ "Register and operate a local Claude Code/Codex executor advertisement"
3235
+ );
3236
+ executor.command("install").description("Configure this checkout's harness to advertise itself as a WLP executor").option("--connector <id>", "Approved local-session ConnectorDefinition id").option("--instance-key <key>", "Stable executor identity (defaults to .sechroom/lane.json code-lane)").option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option("--capability <key...>", "Capability operation keys claimed by this instance").option("--relay <id>", "Relay identity shared by sibling instances", "sechroom-cli-local").option("--subscription-name <name>", "SignalR delivery binding name", "executor-dispatch").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option("--refresh-after <seconds>", "Minimum age before a hook refreshes", parseInteger, 40).option("-y, --yes", "Non-interactive: accept detected surface and lane defaults", false).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
3237
+ const globals = cmd.optsWithGlobals();
3238
+ const lane = readSem()?.values["code-lane"];
3239
+ const detected = detectHookSurfaces(process.cwd());
3240
+ let surface = opts.surface;
3241
+ let instanceKey = opts.instanceKey;
3242
+ let runtime = opts.runtime;
3243
+ let connector = opts.connector;
3244
+ let capabilities = opts.capability;
3245
+ const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
3246
+ if (!opts.yes && canPrompt()) {
3247
+ surface = await promptText("Harness surface (claude or codex)?", surface ?? surfaceDefault);
3248
+ instanceKey = await promptText("Executor instance key?", instanceKey ?? lane ?? "");
3249
+ runtime = await promptText("Runtime (claude-code or codex)?", runtime ?? (surface === "codex" ? "codex" : "claude-code"));
3250
+ connector = await promptText("Approved local-session connector id?", connector ?? "");
3251
+ const capabilityText = await promptText("Capability keys (comma-separated; blank for none)?", capabilities?.join(",") ?? "");
3252
+ capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
3253
+ }
3254
+ surface ??= surfaceDefault;
3255
+ instanceKey ??= lane;
3256
+ runtime ??= surface === "codex" ? "codex" : "claude-code";
3257
+ if (!connector) fail("executor install requires --connector (or an interactive connector id)");
3258
+ if (!instanceKey) fail("no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane");
3259
+ if (!opts.yes && !canPrompt()) fail("non-interactive executor install requires --yes");
3260
+ parseRuntimeKind(runtime);
3261
+ if (!["claude", "codex"].includes(surface)) fail("surface must be claude or codex");
3262
+ if (opts.refreshAfter >= opts.ttl) fail("refresh-after must be shorter than the TTL");
3263
+ const sem = readSem();
3264
+ const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
3265
+ const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
3266
+ const state = {
3267
+ schemaVersion: 1,
3268
+ instanceKey,
3269
+ runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
3270
+ connectorId: connector,
3271
+ capabilityKeys: capabilities ?? [],
3272
+ relayId: opts.relay,
3273
+ subscriptionName: opts.subscriptionName,
3274
+ ttlSeconds: opts.ttl,
3275
+ refreshAfterSeconds: opts.refreshAfter
3276
+ };
3277
+ if (!opts.dryRun) {
3278
+ mkdirSync9(dirname8(statePath), { recursive: true });
3279
+ writeFileSync9(statePath, JSON.stringify(state, null, 2) + "\n");
3280
+ ensureStateDirIgnored(checkout);
3281
+ }
3282
+ const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map((target) => target.dir) : [join11(checkout, ".claude")];
3283
+ const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
3284
+ const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
3285
+ for (const target of hookTargets) {
3286
+ const results = surface === "claude" ? [installClaudeCommands(target, CLAUDE_EXECUTOR_HOOKS, opts.dryRun)] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
3287
+ for (const result of results) process.stderr.write(describe(result, opts.dryRun) + "\n");
3288
+ }
3289
+ warnIfSechroomNotOnPath();
3290
+ process.stderr.write(style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
3291
+ `));
3292
+ });
3293
+ executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
3294
+ await drainStdin();
3295
+ const located = readExecutorState();
3296
+ if (!located) return;
3297
+ const { state, path } = located;
3298
+ const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
3299
+ if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
3300
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3301
+ try {
3302
+ let data;
3303
+ if (state.instanceId) {
3304
+ try {
3305
+ data = await refresh(cfg, state.instanceId, state.ttlSeconds);
3306
+ } catch {
3307
+ data = await registerInstance(cfg, state);
3308
+ }
3309
+ } else {
3310
+ data = await registerInstance(cfg, state);
3311
+ }
3312
+ state.instanceId = data.id;
3313
+ state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
3314
+ writeFileSync9(path, JSON.stringify(state, null, 2) + "\n");
3315
+ } catch {
3316
+ }
3317
+ });
3318
+ executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
3319
+ await drainStdin();
3320
+ const located = readExecutorState();
3321
+ if (!located?.state.instanceId) return;
3322
+ try {
3323
+ await api(resolveConfig(cmd.optsWithGlobals()), `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`, { method: "DELETE", body: JSON.stringify({}) });
3324
+ delete located.state.instanceId;
3325
+ delete located.state.lastRefreshAt;
3326
+ writeFileSync9(located.path, JSON.stringify(located.state, null, 2) + "\n");
3327
+ } catch {
3328
+ }
3329
+ });
3330
+ executor.command("submit-connector").description(
3331
+ "Submit a local-session connector definition for governed approval"
3332
+ ).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
3333
+ "base",
3334
+ "dotnet-10"
3335
+ ]).action(async (opts, cmd) => {
3336
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3337
+ const data = await api(cfg, "/connectors/definitions", {
3338
+ method: "POST",
3339
+ body: JSON.stringify({
3340
+ slug: opts.slug,
3341
+ displayName: opts.displayName,
3342
+ runtimeProfiles: opts.profile,
3343
+ connectorKind: "ExecutionRuntime",
3344
+ providerKind: "local-session",
3345
+ dispatchTransport: parseTransport(opts.transport)
3346
+ })
3347
+ });
3348
+ emit(data, Boolean(cmd.optsWithGlobals().json));
3349
+ if (!cmd.optsWithGlobals().json) {
3350
+ process.stderr.write(
3351
+ style.dim(
3352
+ "approve this connector definition before registering executors\n"
3353
+ )
3354
+ );
3355
+ }
3356
+ });
3357
+ executor.command("register").description(
3358
+ "Create/reuse a SignalR binding and register this local executor instance"
3359
+ ).requiredOption(
3360
+ "--instance-key <key>",
3361
+ "Stable key for this concrete session/lane"
3362
+ ).requiredOption(
3363
+ "--connector <id>",
3364
+ "Approved local-session ConnectorDefinition id"
3365
+ ).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
3366
+ "--relay <id>",
3367
+ "Relay identity shared by sibling instances",
3368
+ "sechroom-cli-local"
3369
+ ).option(
3370
+ "--subscription-name <name>",
3371
+ "SignalR delivery binding name",
3372
+ "executor-dispatch"
3373
+ ).option(
3374
+ "--capability <key...>",
3375
+ "Capability operation keys claimed by this instance"
3376
+ ).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
3377
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3378
+ if (!cfg.workspaceId)
3379
+ fail("executor register requires a configured workspaceId");
3380
+ const subscription = await api(
3381
+ cfg,
3382
+ "/me/delivery-subscriptions/signalr",
3383
+ {
3384
+ method: "POST",
3385
+ body: JSON.stringify({
3386
+ name: opts.subscriptionName,
3387
+ enabled: true,
3388
+ filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
3389
+ })
3390
+ }
3391
+ );
3392
+ const data = await api(
3393
+ cfg,
3394
+ "/me/executor-instances",
3395
+ {
3396
+ method: "POST",
3397
+ body: JSON.stringify({
3398
+ relayId: opts.relay,
3399
+ instanceKey: opts.instanceKey,
3400
+ runtimeKind: parseRuntimeKind(opts.runtime),
3401
+ activationMode: "Attached",
3402
+ deliverySubscriptionId: subscription.id,
3403
+ connectorId: opts.connector,
3404
+ claimedCapabilityKeys: opts.capability ?? [],
3405
+ toolSetRef: opts.toolSetRef ?? null,
3406
+ ttlSeconds: opts.ttl
3407
+ })
3408
+ }
3409
+ );
3410
+ emit(data, Boolean(cmd.optsWithGlobals().json));
3411
+ if (!cmd.optsWithGlobals().json)
3412
+ process.stderr.write(
3413
+ style.dim(`refresh with: sechroom executor heartbeat ${data.id}
3414
+ `)
3415
+ );
3416
+ });
3417
+ executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
3418
+ const data = await refresh(
3419
+ resolveConfig(cmd.optsWithGlobals()),
3420
+ id,
3421
+ opts.ttl
3422
+ );
3423
+ emit(data, Boolean(cmd.optsWithGlobals().json));
3424
+ });
3425
+ executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
3426
+ if (opts.interval >= opts.ttl)
3427
+ fail("heartbeat interval must be shorter than the TTL");
3428
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3429
+ await refresh(cfg, id, opts.ttl);
3430
+ process.stderr.write(
3431
+ style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
3432
+ `)
3433
+ );
3434
+ await holdHeartbeat(async () => {
3435
+ await refresh(cfg, id, opts.ttl);
3436
+ }, opts.interval * 1e3);
3437
+ });
3438
+ executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
3439
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3440
+ const data = await api(
3441
+ cfg,
3442
+ `/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
3443
+ );
3444
+ emit(data, Boolean(cmd.optsWithGlobals().json));
3445
+ });
3446
+ executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
3447
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3448
+ const data = await api(
3449
+ cfg,
3450
+ `/me/executor-instances/${encodeURIComponent(id)}`,
3451
+ {
3452
+ method: "DELETE",
3453
+ body: JSON.stringify({})
3454
+ }
3455
+ );
3456
+ emit(data, Boolean(cmd.optsWithGlobals().json));
3457
+ });
3458
+ }
3459
+ function parseRuntimeKind(value) {
3460
+ switch (value.trim().toLowerCase()) {
3461
+ case "claude":
3462
+ case "claude-code":
3463
+ return "ClaudeCode";
3464
+ case "codex":
3465
+ return "Codex";
3466
+ default:
3467
+ return fail("runtime must be claude-code or codex");
3468
+ }
3469
+ }
3470
+ function parseTransport(value) {
3471
+ switch (value.trim().toLowerCase()) {
3472
+ case "push":
3473
+ return "Push";
3474
+ case "pull":
3475
+ return "Pull";
3476
+ default:
3477
+ return fail("transport must be push or pull");
3478
+ }
3479
+ }
3480
+ async function refresh(cfg, id, ttlSeconds) {
3481
+ return api(
3482
+ cfg,
3483
+ `/me/executor-instances/${encodeURIComponent(id)}/refresh`,
3484
+ {
3485
+ method: "POST",
3486
+ body: JSON.stringify({ ttlSeconds })
3487
+ }
3488
+ );
3489
+ }
3490
+ async function registerInstance(cfg, state) {
3491
+ if (!cfg.workspaceId) fail("executor registration requires a configured workspaceId");
3492
+ const subscription = await api(cfg, "/me/delivery-subscriptions/signalr", {
3493
+ method: "POST",
3494
+ body: JSON.stringify({ name: state.subscriptionName, enabled: true, filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] } })
3495
+ });
3496
+ return api(cfg, "/me/executor-instances", {
3497
+ method: "POST",
3498
+ body: JSON.stringify({ relayId: state.relayId, instanceKey: state.instanceKey, runtimeKind: parseRuntimeKind(state.runtime), activationMode: "Attached", deliverySubscriptionId: subscription.id, connectorId: state.connectorId, claimedCapabilityKeys: state.capabilityKeys, toolSetRef: null, ttlSeconds: state.ttlSeconds })
3499
+ });
3500
+ }
3501
+ function readExecutorState(start = process.cwd()) {
3502
+ const semPath = resolveSemPathForRead(start);
3503
+ const sem = semPath ? readSem(semPath) : void 0;
3504
+ const path = join11(sem ? dirname8(sem.path) : join11(start, ".sechroom"), EXECUTOR_STATE);
3505
+ if (!existsSync9(path)) return void 0;
3506
+ return { state: JSON.parse(readFileSync8(path, "utf8")), path };
3507
+ }
3508
+ async function drainStdin() {
3509
+ if (process.stdin.isTTY) return;
3510
+ for await (const _chunk of process.stdin) {
3511
+ }
3512
+ }
3513
+ async function api(cfg, path, init) {
3514
+ const token = await requireToken(cfg);
3515
+ const response = await fetch(`${cfg.baseUrl}${path}`, {
3516
+ ...init,
3517
+ headers: {
3518
+ authorization: `Bearer ${token}`,
3519
+ tenant: cfg.tenant,
3520
+ "content-type": "application/json",
3521
+ "x-sechroom-surface": "cli"
3522
+ }
3523
+ });
3524
+ if (!response.ok)
3525
+ fail(
3526
+ `${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
3527
+ );
3528
+ return response.json();
3529
+ }
3530
+ function parseInteger(value) {
3531
+ const parsed = Number.parseInt(value, 10);
3532
+ if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
3533
+ return parsed;
3534
+ }
3535
+ function holdHeartbeat(tick, intervalMs) {
3536
+ return new Promise((resolve3, reject) => {
3537
+ const timer = setInterval(() => void tick().catch(reject), intervalMs);
3538
+ const stop = () => {
3539
+ clearInterval(timer);
3540
+ resolve3();
3541
+ };
3542
+ process.once("SIGINT", stop);
3543
+ process.once("SIGTERM", stop);
3544
+ });
3545
+ }
3546
+
2696
3547
  // src/commands/filing.ts
2697
3548
  function registerFiling(program2) {
2698
3549
  const filing = program2.command("filing").description("Review and act on filing suggestions");
@@ -3204,8 +4055,8 @@ Examples:
3204
4055
 
3205
4056
  // src/setup/apply.ts
3206
4057
  import { createHash as createHash3 } from "crypto";
3207
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3208
- import { dirname as dirname8 } from "path";
4058
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
4059
+ import { dirname as dirname9 } from "path";
3209
4060
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3210
4061
  var MARKER_END = "<!-- @sechroom/cli:end";
3211
4062
  function normalizeBody(s) {
@@ -3258,22 +4109,22 @@ function parseManagedBlock(content, block) {
3258
4109
  return null;
3259
4110
  }
3260
4111
  function ensureDir2(path) {
3261
- mkdirSync9(dirname8(path), { recursive: true });
4112
+ mkdirSync10(dirname9(path), { recursive: true });
3262
4113
  }
3263
4114
  function readOr(path, fallback) {
3264
4115
  try {
3265
- return readFileSync7(path, "utf8");
4116
+ return readFileSync9(path, "utf8");
3266
4117
  } catch {
3267
4118
  return fallback;
3268
4119
  }
3269
4120
  }
3270
4121
  function mergeMcpJson(path, snippet, dryRun) {
3271
4122
  const incoming = JSON.parse(snippet);
3272
- const existed = existsSync9(path);
4123
+ const existed = existsSync10(path);
3273
4124
  let current = {};
3274
4125
  if (existed) {
3275
4126
  try {
3276
- current = JSON.parse(readFileSync7(path, "utf8"));
4127
+ current = JSON.parse(readFileSync9(path, "utf8"));
3277
4128
  } catch {
3278
4129
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3279
4130
  }
@@ -3281,26 +4132,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3281
4132
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3282
4133
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3283
4134
  ensureDir2(path);
3284
- writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
4135
+ writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3285
4136
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3286
4137
  }
3287
4138
  function mergeCodexToml(path, snippet, dryRun) {
3288
- const existed = existsSync9(path);
4139
+ const existed = existsSync10(path);
3289
4140
  let body = readOr(path, "");
3290
4141
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3291
4142
  const trimmed = body.trim();
3292
4143
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3293
4144
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3294
4145
  ensureDir2(path);
3295
- writeFileSync9(path, next, { mode: 384 });
4146
+ writeFileSync10(path, next, { mode: 384 });
3296
4147
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3297
4148
  }
3298
4149
  function writeInstructionBlock(path, write, dryRun) {
3299
- const existed = existsSync9(path);
4150
+ const existed = existsSync10(path);
3300
4151
  const next = computeBlockFile(readOr(path, ""), write);
3301
4152
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3302
4153
  ensureDir2(path);
3303
- writeFileSync9(path, next);
4154
+ writeFileSync10(path, next);
3304
4155
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3305
4156
  }
3306
4157
  function computeBlockFile(current, write) {
@@ -3341,7 +4192,7 @@ function applyBlock(path, write, mode, dryRun) {
3341
4192
  const next = computeBlockFile(current, write);
3342
4193
  if (!dryRun) {
3343
4194
  ensureDir2(proposedPath);
3344
- writeFileSync9(proposedPath, next);
4195
+ writeFileSync10(proposedPath, next);
3345
4196
  }
3346
4197
  return {
3347
4198
  kind: "instruction",
@@ -3471,8 +4322,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3471
4322
  }
3472
4323
 
3473
4324
  // src/setup/skills-offer.ts
3474
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
3475
- import { join as join11 } from "path";
4325
+ import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
4326
+ import { join as join12 } from "path";
3476
4327
 
3477
4328
  // src/setup/lane-pin.ts
3478
4329
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3588,8 +4439,8 @@ Found ${summary} available to you for ${surface}.
3588
4439
  if (skills.length > 0) {
3589
4440
  const written = [];
3590
4441
  for (const s of skills) {
3591
- mkdirSync10(join11(sDir, s.name), { recursive: true });
3592
- writeFileSync10(join11(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
4442
+ mkdirSync11(join12(sDir, s.name), { recursive: true });
4443
+ writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3593
4444
  written.push(s.name);
3594
4445
  }
3595
4446
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3597,11 +4448,11 @@ Found ${summary} available to you for ${surface}.
3597
4448
  `);
3598
4449
  }
3599
4450
  if (agents.length > 0) {
3600
- mkdirSync10(aDir, { recursive: true });
4451
+ mkdirSync11(aDir, { recursive: true });
3601
4452
  const written = [];
3602
4453
  for (const a of agents) {
3603
4454
  const file = `${a.name}.md`;
3604
- writeFileSync10(join11(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
4455
+ writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3605
4456
  written.push(file);
3606
4457
  }
3607
4458
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3984,13 +4835,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3984
4835
  }
3985
4836
 
3986
4837
  // src/commands/onboard.ts
3987
- import { existsSync as existsSync11 } from "fs";
3988
- import { basename as basename2, join as join13 } from "path";
4838
+ import { existsSync as existsSync12 } from "fs";
4839
+ import { basename as basename2, join as join14 } from "path";
3989
4840
 
3990
4841
  // src/commands/fanout.ts
3991
4842
  import { spawnSync } from "child_process";
3992
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3993
- import { isAbsolute, join as join12, resolve } from "path";
4843
+ import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
4844
+ import { isAbsolute, join as join13, resolve } from "path";
3994
4845
  var ICON = {
3995
4846
  refresh: "\u21BB",
3996
4847
  bind: "+",
@@ -4010,21 +4861,21 @@ function discoverChildren(root) {
4010
4861
  const out = [];
4011
4862
  for (const name of names.sort()) {
4012
4863
  if (name.startsWith(".") || name === "node_modules") continue;
4013
- const dir = join12(root, name);
4864
+ const dir = join13(root, name);
4014
4865
  try {
4015
4866
  if (!statSync3(dir).isDirectory()) continue;
4016
4867
  } catch {
4017
4868
  continue;
4018
4869
  }
4019
- if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
4870
+ if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
4020
4871
  }
4021
4872
  return out;
4022
4873
  }
4023
4874
  function readManifest(path) {
4024
- if (!existsSync10(path)) return null;
4875
+ if (!existsSync11(path)) return null;
4025
4876
  let parsed;
4026
4877
  try {
4027
- parsed = JSON.parse(readFileSync8(path, "utf8"));
4878
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
4028
4879
  } catch (err2) {
4029
4880
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
4030
4881
  }
@@ -4390,10 +5241,10 @@ async function chooseScope(scopeFlag, yes) {
4390
5241
  }
4391
5242
  async function planRecurseChild(entry, root, client, opts) {
4392
5243
  const dir = resolveChildDir(entry.path, root);
4393
- if (!existsSync11(dir)) {
5244
+ if (!existsSync12(dir)) {
4394
5245
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4395
5246
  }
4396
- if (existsSync11(join13(dir, ".sechroom.json"))) {
5247
+ if (existsSync12(join14(dir, ".sechroom.json"))) {
4397
5248
  return {
4398
5249
  label: entry.path,
4399
5250
  dir,
@@ -4466,7 +5317,7 @@ This fan-out will pin the same lane in every repo:
4466
5317
  async function runRecurse(cfg, g, opts) {
4467
5318
  const { yes, dryRun, json } = opts;
4468
5319
  const root = process.cwd();
4469
- const manifestPath = join13(root, ".sechroom", "repos.json");
5320
+ const manifestPath = join14(root, ".sechroom", "repos.json");
4470
5321
  const fromManifest = readManifest(manifestPath);
4471
5322
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4472
5323
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4838,24 +5689,45 @@ Examples:
4838
5689
  $ sechroom relationship suggestions --status Pending --memory mem_XXXX
4839
5690
  $ sechroom relationship suggestion accept rsg_XXXX`
4840
5691
  );
4841
- relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").action(async (fromMemoryId, toMemoryId, opts, cmd) => {
5692
+ relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
5693
+ "--to-version <number>",
5694
+ "Version of the target memory to pin the edge to (defaults to the target's current version)"
5695
+ ).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
4842
5696
  const cfg = resolveConfig(cmd.optsWithGlobals());
4843
- const data = await runApi("Creating relationship", async () => {
4844
- const client = await makeClient(cfg);
4845
- return client.POST("/memories/{memoryId}/relationships", {
5697
+ const client = await makeClient(cfg);
5698
+ let toVersion;
5699
+ if (opts.toVersion !== void 0) {
5700
+ toVersion = Number(opts.toVersion);
5701
+ if (!Number.isInteger(toVersion) || toVersion < 1) {
5702
+ fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
5703
+ }
5704
+ } else {
5705
+ const target = await runApi(
5706
+ "Resolving target version",
5707
+ async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
5708
+ );
5709
+ if (typeof target.item?.currentVersion !== "number") {
5710
+ fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
5711
+ }
5712
+ toVersion = target.item.currentVersion;
5713
+ }
5714
+ const data = await runApi(
5715
+ "Creating relationship",
5716
+ async () => client.POST("/memories/{memoryId}/relationships", {
4846
5717
  params: { path: { memoryId: fromMemoryId } },
4847
5718
  body: {
4848
5719
  toMemoryId,
4849
- type: opts.type
5720
+ type: opts.type,
5721
+ toVersion
4850
5722
  }
4851
- });
4852
- });
5723
+ })
5724
+ );
4853
5725
  const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
4854
5726
  const view = resolveViewUrl(cfg.baseUrl, data.url);
4855
5727
  const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
4856
5728
  emitAction(
4857
- `created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
4858
- data,
5729
+ `created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
5730
+ { ...data, toVersion },
4859
5731
  cmd.optsWithGlobals().json
4860
5732
  );
4861
5733
  });
@@ -4978,23 +5850,23 @@ Examples:
4978
5850
 
4979
5851
  // src/commands/reset.ts
4980
5852
  import { homedir as homedir4 } from "os";
4981
- import { join as join14 } from "path";
4982
- import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
5853
+ import { join as join15 } from "path";
5854
+ import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
4983
5855
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4984
- var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
4985
- var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
4986
- var localAgentsDir = () => join14(process.cwd(), ".claude", "agents");
4987
- var globalAgentsDir = () => join14(homedir4(), ".claude", "agents");
5856
+ var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
5857
+ var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
5858
+ var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
5859
+ var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
4988
5860
  function removeMaterialisedSkills(dir) {
4989
5861
  const removed = [];
4990
- const lockPath = join14(dir, SKILLS_LOCK2);
4991
- if (!existsSync12(lockPath)) return removed;
5862
+ const lockPath = join15(dir, SKILLS_LOCK2);
5863
+ if (!existsSync13(lockPath)) return removed;
4992
5864
  try {
4993
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
5865
+ const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
4994
5866
  for (const entry of Object.values(lock)) {
4995
5867
  for (const name of entry.skills ?? []) {
4996
- const p = join14(dir, name);
4997
- if (existsSync12(p)) {
5868
+ const p = join15(dir, name);
5869
+ if (existsSync13(p)) {
4998
5870
  rmSync3(p, { recursive: true, force: true });
4999
5871
  removed.push(p);
5000
5872
  }
@@ -5039,18 +5911,18 @@ function registerReset(program2) {
5039
5911
  }
5040
5912
  }
5041
5913
  const removed = [];
5042
- const stateDir = join14(process.cwd(), ".sechroom");
5043
- if (existsSync12(stateDir)) {
5914
+ const stateDir = join15(process.cwd(), ".sechroom");
5915
+ if (existsSync13(stateDir)) {
5044
5916
  rmSync3(stateDir, { recursive: true, force: true });
5045
5917
  removed.push(stateDir);
5046
5918
  }
5047
- const legacyCfg = join14(process.cwd(), ".sechroom.json");
5048
- if (existsSync12(legacyCfg)) {
5919
+ const legacyCfg = join15(process.cwd(), ".sechroom.json");
5920
+ if (existsSync13(legacyCfg)) {
5049
5921
  rmSync3(legacyCfg, { force: true });
5050
5922
  removed.push(legacyCfg);
5051
5923
  }
5052
- const legacySem = join14(process.cwd(), ".sem");
5053
- if (existsSync12(legacySem)) {
5924
+ const legacySem = join15(process.cwd(), ".sem");
5925
+ if (existsSync13(legacySem)) {
5054
5926
  rmSync3(legacySem, { force: true });
5055
5927
  removed.push(legacySem);
5056
5928
  }
@@ -5076,8 +5948,8 @@ function registerReset(program2) {
5076
5948
  }
5077
5949
 
5078
5950
  // src/commands/skills.ts
5079
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, statSync as statSync4, writeFileSync as writeFileSync11 } from "fs";
5080
- import { join as join15 } from "path";
5951
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
5952
+ import { join as join16 } from "path";
5081
5953
  function filenameFromDisposition(header) {
5082
5954
  if (!header) return void 0;
5083
5955
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -5085,11 +5957,11 @@ function filenameFromDisposition(header) {
5085
5957
  }
5086
5958
  function resolveOutputPath(output, serverFilename) {
5087
5959
  const filename = serverFilename || "skills.zip";
5088
- if (!output) return join15(process.cwd(), filename);
5089
- const looksLikeDir = output.endsWith("/") || existsSync13(output) && statSync4(output).isDirectory();
5960
+ if (!output) return join16(process.cwd(), filename);
5961
+ const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
5090
5962
  if (looksLikeDir) {
5091
- mkdirSync11(output, { recursive: true });
5092
- return join15(output, filename);
5963
+ mkdirSync12(output, { recursive: true });
5964
+ return join16(output, filename);
5093
5965
  }
5094
5966
  return output;
5095
5967
  }
@@ -5120,7 +5992,7 @@ async function downloadZip(label, call, output) {
5120
5992
  const buf = Buffer.from(res.data);
5121
5993
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
5122
5994
  const path = resolveOutputPath(output, filename);
5123
- writeFileSync11(path, buf);
5995
+ writeFileSync12(path, buf);
5124
5996
  return { path, bytes: buf.length, filename };
5125
5997
  }
5126
5998
  function registerSkills(program2) {
@@ -5129,7 +6001,9 @@ function registerSkills(program2) {
5129
6001
  "after",
5130
6002
  `
5131
6003
  Examples:
5132
- $ sechroom skills install materialise your installed skills to ~/.claude/skills
6004
+ $ sechroom skills install --client claude materialise target:claude-code skills to ~/.claude/skills
6005
+ $ sechroom skills install --client codex materialise target:gpt-codex skills to ~/.codex/skills
6006
+ $ sechroom skills install --client all keep both global clients in sync
5133
6007
  $ sechroom skills install --scope project write them to ./.claude/skills instead
5134
6008
  $ sechroom skills list what's materialised on disk
5135
6009
  $ sechroom skills clean remove the materialised skill files
@@ -5138,11 +6012,15 @@ Examples:
5138
6012
  $ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
5139
6013
  $ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
5140
6014
 
6015
+ When --client is omitted, configured client flags/environment variables win;
6016
+ otherwise existing default client homes are detected. Both configured/detected
6017
+ clients select all; an unconfigured machine preserves the legacy Claude default.
6018
+
5141
6019
  `
5142
6020
  );
5143
- 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("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
5144
- 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("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
5145
- 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("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
6021
+ 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));
6022
+ 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));
6023
+ 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));
5146
6024
  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) => {
5147
6025
  const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
5148
6026
  const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
@@ -5288,12 +6166,12 @@ Examples:
5288
6166
  }
5289
6167
 
5290
6168
  // src/commands/sweep.ts
5291
- import { existsSync as existsSync14 } from "fs";
5292
- import { dirname as dirname9, join as join16, resolve as resolve2 } from "path";
5293
- var DEFAULT_MANIFEST = join16(".sechroom", "repos.json");
6169
+ import { existsSync as existsSync15 } from "fs";
6170
+ import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
6171
+ var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
5294
6172
  function planEntry(entry, root) {
5295
6173
  const dir = resolveChildDir(entry.path, root);
5296
- if (!existsSync14(dir)) {
6174
+ if (!existsSync15(dir)) {
5297
6175
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5298
6176
  }
5299
6177
  if (committedBindingPath(dir)) {
@@ -5369,7 +6247,7 @@ Examples:
5369
6247
  `);
5370
6248
  return;
5371
6249
  }
5372
- const root = dirname9(dirname9(manifestPath));
6250
+ const root = dirname10(dirname10(manifestPath));
5373
6251
  const plans = repos.map((entry) => planEntry(entry, root));
5374
6252
  if (!json) {
5375
6253
  process.stderr.write(
@@ -5388,13 +6266,13 @@ Examples:
5388
6266
 
5389
6267
  // src/commands/telemetry.ts
5390
6268
  import {
5391
- existsSync as existsSync15,
5392
- mkdirSync as mkdirSync12,
5393
- readFileSync as readFileSync10,
6269
+ existsSync as existsSync16,
6270
+ mkdirSync as mkdirSync13,
6271
+ readFileSync as readFileSync12,
5394
6272
  rmSync as rmSync4,
5395
- writeFileSync as writeFileSync12
6273
+ writeFileSync as writeFileSync13
5396
6274
  } from "fs";
5397
- import { dirname as dirname10, join as join17 } from "path";
6275
+ import { dirname as dirname11, join as join18 } from "path";
5398
6276
  function registerTelemetry(program2) {
5399
6277
  const telemetry = program2.command("telemetry").description(
5400
6278
  "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
@@ -5464,14 +6342,14 @@ function registerTelemetry(program2) {
5464
6342
  "Decomposition id this session executes"
5465
6343
  ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5466
6344
  const json = Boolean(cmd.optsWithGlobals().json);
5467
- const dir = join17(process.cwd(), ".sechroom");
5468
- mkdirSync12(dir, { recursive: true });
5469
- const path = join17(dir, BINDING_FILE);
6345
+ const dir = join18(process.cwd(), ".sechroom");
6346
+ mkdirSync13(dir, { recursive: true });
6347
+ const path = join18(dir, BINDING_FILE);
5470
6348
  const binding = {
5471
6349
  decompositionId: opts.decomposition,
5472
6350
  taskId: opts.task
5473
6351
  };
5474
- writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
6352
+ writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
5475
6353
  ensureStateDirIgnored(process.cwd());
5476
6354
  if (json) {
5477
6355
  emit({ bound: true, ...binding, path }, true);
@@ -5486,8 +6364,8 @@ function registerTelemetry(program2) {
5486
6364
  });
5487
6365
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5488
6366
  const json = Boolean(cmd.optsWithGlobals().json);
5489
- const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
5490
- const existed = existsSync15(path);
6367
+ const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
6368
+ const existed = existsSync16(path);
5491
6369
  if (existed) rmSync4(path);
5492
6370
  if (json) emit({ unbound: existed, path }, true);
5493
6371
  else
@@ -5496,7 +6374,7 @@ function registerTelemetry(program2) {
5496
6374
  );
5497
6375
  });
5498
6376
  telemetry.command("hook").description(
5499
- "Per-turn telemetry self-report for a Claude Code Stop hook (reads stdin; no-op unless bound). Fail-soft."
6377
+ "Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
5500
6378
  ).action(async (_opts, cmd) => {
5501
6379
  try {
5502
6380
  const raw = await readStdin2();
@@ -5505,21 +6383,10 @@ function registerTelemetry(program2) {
5505
6383
  const binding = findBinding(cwd);
5506
6384
  if (!binding) return process.exit(0);
5507
6385
  const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
5508
- if (!usage) return process.exit(0);
6386
+ const events = buildHookEvents(input, usage, binding.taskId);
6387
+ if (events.length === 0) return process.exit(0);
5509
6388
  const cfg = resolveConfig(cmd.optsWithGlobals());
5510
- await postTelemetry(cfg, binding.decompositionId, [
5511
- {
5512
- taskId: binding.taskId,
5513
- kind: "Parsed",
5514
- tokensIn: usage.tokensIn,
5515
- tokensOut: usage.tokensOut,
5516
- contextUsed: usage.contextUsed,
5517
- contextWindow: usage.contextWindow,
5518
- text: null,
5519
- approvalState: null,
5520
- verdict: null
5521
- }
5522
- ]);
6389
+ await postTelemetry(cfg, binding.decompositionId, events);
5523
6390
  return process.exit(0);
5524
6391
  } catch {
5525
6392
  return process.exit(0);
@@ -5547,7 +6414,12 @@ function registerTelemetry(program2) {
5547
6414
  scope,
5548
6415
  cwd
5549
6416
  });
5550
- const commands = { Stop: "sechroom telemetry hook" };
6417
+ const commands = {
6418
+ Stop: "sechroom telemetry hook",
6419
+ SubagentStop: "sechroom telemetry hook",
6420
+ Notification: "sechroom telemetry hook",
6421
+ PermissionDenied: "sechroom telemetry hook"
6422
+ };
5551
6423
  try {
5552
6424
  const multi = targets.length > 1;
5553
6425
  const results = targets.map((t) => {
@@ -5601,11 +6473,11 @@ async function postTelemetry(cfg, decompositionId, events) {
5601
6473
  function findBinding(start) {
5602
6474
  let dir = start;
5603
6475
  for (; ; ) {
5604
- const path = join17(dir, ".sechroom", BINDING_FILE);
5605
- if (existsSync15(path)) {
6476
+ const path = join18(dir, ".sechroom", BINDING_FILE);
6477
+ if (existsSync16(path)) {
5606
6478
  try {
5607
6479
  const b = JSON.parse(
5608
- readFileSync10(path, "utf8")
6480
+ readFileSync12(path, "utf8")
5609
6481
  );
5610
6482
  if (b.decompositionId && b.taskId)
5611
6483
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5613,18 +6485,18 @@ function findBinding(start) {
5613
6485
  }
5614
6486
  return null;
5615
6487
  }
5616
- const parent = dirname10(dir);
6488
+ const parent = dirname11(dir);
5617
6489
  if (parent === dir) return null;
5618
6490
  dir = parent;
5619
6491
  }
5620
6492
  }
5621
6493
  function parseTranscript(path) {
5622
- if (!existsSync15(path)) return null;
6494
+ if (!existsSync16(path)) return null;
5623
6495
  let tokensIn = 0;
5624
6496
  let tokensOut = 0;
5625
6497
  let contextUsed = 0;
5626
6498
  let model = "";
5627
- for (const line of readFileSync10(path, "utf8").split("\n")) {
6499
+ for (const line of readFileSync12(path, "utf8").split("\n")) {
5628
6500
  if (!line.trim()) continue;
5629
6501
  let obj;
5630
6502
  try {
@@ -5641,11 +6513,61 @@ function parseTranscript(path) {
5641
6513
  if (obj.message?.model) model = obj.message.model;
5642
6514
  }
5643
6515
  if (tokensIn === 0 && tokensOut === 0) return null;
5644
- return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
6516
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
5645
6517
  }
5646
- function windowFor(model) {
6518
+ function windowFor(model, contextUsed = 0) {
5647
6519
  const m = model.toLowerCase();
5648
- return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
6520
+ if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
6521
+ return contextUsed > 2e5 ? 1e6 : 2e5;
6522
+ }
6523
+ function buildHookEvents(input, usage, taskId) {
6524
+ const events = [];
6525
+ const base = (kind, over) => ({
6526
+ taskId,
6527
+ kind,
6528
+ tokensIn: null,
6529
+ tokensOut: null,
6530
+ contextUsed: null,
6531
+ contextWindow: null,
6532
+ text: null,
6533
+ approvalState: null,
6534
+ verdict: null,
6535
+ ...over
6536
+ });
6537
+ if (usage) {
6538
+ events.push(
6539
+ base("Parsed", {
6540
+ tokensIn: usage.tokensIn,
6541
+ tokensOut: usage.tokensOut,
6542
+ contextUsed: usage.contextUsed,
6543
+ contextWindow: usage.contextWindow
6544
+ })
6545
+ );
6546
+ }
6547
+ switch (input.hook_event_name) {
6548
+ case "PermissionDenied":
6549
+ events.push(
6550
+ base("Approval", {
6551
+ approvalState: "denied",
6552
+ text: input.tool_name ?? input.message ?? null
6553
+ })
6554
+ );
6555
+ break;
6556
+ case "Notification":
6557
+ if (isPermissionNotification(input))
6558
+ events.push(base("Approval", { text: input.message ?? null }));
6559
+ break;
6560
+ case "Stop":
6561
+ case "SubagentStop":
6562
+ events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
6563
+ break;
6564
+ }
6565
+ return events;
6566
+ }
6567
+ function isPermissionNotification(input) {
6568
+ const t = (input.notification_type ?? input.type ?? "").toLowerCase();
6569
+ if (t) return t.includes("permission");
6570
+ return (input.message ?? "").toLowerCase().includes("permission");
5649
6571
  }
5650
6572
  async function readStdin2() {
5651
6573
  if (process.stdin.isTTY) return "";
@@ -5881,7 +6803,7 @@ Examples:
5881
6803
  function resolveVersion() {
5882
6804
  try {
5883
6805
  const pkg = JSON.parse(
5884
- readFileSync11(new URL("../package.json", import.meta.url), "utf8")
6806
+ readFileSync13(new URL("../package.json", import.meta.url), "utf8")
5885
6807
  );
5886
6808
  return pkg.version ?? "0.0.0";
5887
6809
  } catch {
@@ -6039,6 +6961,9 @@ registerLookup(program);
6039
6961
  registerRelationships(program);
6040
6962
  registerWorkspace(program);
6041
6963
  registerProject(program);
6964
+ registerDecomposition(program);
6965
+ registerExecutor(program);
6966
+ registerClose(program);
6042
6967
  registerFiling(program);
6043
6968
  registerContinuity(program);
6044
6969
  registerCheckpoint(program);