@sechroom/cli 2026.7.2 → 2026.7.3-rc.f271682b

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 +849 -432
  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 readFileSync10 } from "fs";
4
+ import { readFileSync as readFileSync12 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -1300,12 +1300,228 @@ workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
1300
1300
  }
1301
1301
 
1302
1302
  // src/commands/channel.ts
1303
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1304
+ import { dirname as dirname4, join as join7 } from "path";
1303
1305
  import {
1304
1306
  HttpTransportType,
1305
1307
  HubConnectionBuilder
1306
1308
  } from "@microsoft/signalr";
1307
1309
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1308
1310
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1311
+
1312
+ // src/commands/hook-install.ts
1313
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1314
+ import { delimiter, dirname as dirname3, join as join6 } from "path";
1315
+
1316
+ // src/setup/clients.ts
1317
+ import { existsSync as existsSync4 } from "fs";
1318
+ import { homedir as homedir3 } from "os";
1319
+ import { dirname as dirname2, join as join5 } from "path";
1320
+ function claudeDesktopConfigPath(home) {
1321
+ switch (process.platform) {
1322
+ case "darwin":
1323
+ return join5(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1324
+ case "win32":
1325
+ return join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1326
+ default:
1327
+ return join5(home, ".config", "Claude", "claude_desktop_config.json");
1328
+ }
1329
+ }
1330
+ function clientTargets(cwd, opts = {}) {
1331
+ const home = homedir3();
1332
+ const claudeDir = opts.claudeDir ?? join5(home, ".claude");
1333
+ const codexHome = opts.codexHome ?? join5(home, ".codex");
1334
+ return {
1335
+ "claude-code": {
1336
+ key: "claude-code",
1337
+ label: "Claude Code",
1338
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".mcp.json"), format: "json" },
1339
+ instruction: { surfaceKey: "claude-code", path: join5(cwd, "CLAUDE.md") }
1340
+ },
1341
+ "claude-desktop": {
1342
+ key: "claude-desktop",
1343
+ label: "Claude Desktop",
1344
+ mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1345
+ instruction: { surfaceKey: "claude-desktop", path: join5(claudeDir, "CLAUDE.md") }
1346
+ },
1347
+ codex: {
1348
+ key: "codex",
1349
+ label: "Codex CLI",
1350
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join5(codexHome, "config.toml"), format: "toml" },
1351
+ instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1352
+ },
1353
+ cursor: {
1354
+ key: "cursor",
1355
+ label: "Cursor",
1356
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".cursor", "mcp.json"), format: "json" },
1357
+ instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1358
+ },
1359
+ antigravity: {
1360
+ key: "antigravity",
1361
+ label: "Google Antigravity",
1362
+ // FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
1363
+ // `~/.gemini/config/mcp_config.json` (not cwd; not affected by
1364
+ // CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
1365
+ // `type` — comes from the `antigravity` server surface, so we don't
1366
+ // hardcode it here. Instructions go in the project `AGENTS.md`
1367
+ // (cross-tool, shared with Codex/Cursor).
1368
+ mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join5(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1369
+ instruction: { surfaceKey: "antigravity", path: join5(cwd, "AGENTS.md") }
1370
+ }
1371
+ };
1372
+ }
1373
+ var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
1374
+ var DEFAULT_CLIENT_KEY = "claude-code";
1375
+ function detectInstalledClients(cwd) {
1376
+ const home = homedir3();
1377
+ const detected = [];
1378
+ if (resolveClaudeTargets({}).some((t) => existsSync4(t.dir))) detected.push("claude-code");
1379
+ if (existsSync4(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1380
+ if (resolveCodexHomes({}).some((d) => existsSync4(d))) detected.push("codex");
1381
+ if (existsSync4(join5(home, ".cursor")) || existsSync4(join5(cwd, ".cursor"))) detected.push("cursor");
1382
+ if (existsSync4(join5(home, ".gemini"))) detected.push("antigravity");
1383
+ return detected;
1384
+ }
1385
+
1386
+ // src/commands/hook-install.ts
1387
+ var CLAUDE_HOOK_COMMANDS = {
1388
+ SessionStart: "sechroom hook session-start",
1389
+ PreCompact: "sechroom hook pre-compact",
1390
+ 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"
1395
+ };
1396
+ var CODEX_HOOK_COMMANDS = {
1397
+ SessionStart: "sechroom hook session-start",
1398
+ Stop: "sechroom hook session-end --debounce-minutes 10"
1399
+ };
1400
+ function hasHookCommand(config2, event, command) {
1401
+ const groups = config2.hooks?.[event] ?? [];
1402
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1403
+ }
1404
+ function mergeHooks(config2, commands) {
1405
+ config2.hooks ??= {};
1406
+ let added = 0;
1407
+ for (const [event, command] of Object.entries(commands)) {
1408
+ if (hasHookCommand(config2, event, command)) continue;
1409
+ const groups = config2.hooks[event] ??= [];
1410
+ groups.push({ hooks: [{ type: "command", command }] });
1411
+ added += 1;
1412
+ }
1413
+ return added;
1414
+ }
1415
+ function readJsonConfig2(path) {
1416
+ if (!existsSync5(path)) return {};
1417
+ const raw = readFileSync3(path, "utf8");
1418
+ if (!raw.trim()) return {};
1419
+ return JSON.parse(raw);
1420
+ }
1421
+ function installHooksJson(path, commands, dryRun) {
1422
+ const existed = existsSync5(path) && readFileSync3(path, "utf8").trim().length > 0;
1423
+ const config2 = readJsonConfig2(path);
1424
+ const added = mergeHooks(config2, commands);
1425
+ if (added === 0 && existed) return { path, status: "current" };
1426
+ if (!dryRun) {
1427
+ mkdirSync4(dirname3(path), { recursive: true });
1428
+ writeFileSync4(path, JSON.stringify(config2, null, 2) + "\n");
1429
+ }
1430
+ return { path, status: existed ? "merged" : "created" };
1431
+ }
1432
+ function installClaudeCommands(claudeDir, commands, dryRun) {
1433
+ return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
1434
+ }
1435
+ function ensureCodexFeaturesHooks(content) {
1436
+ const lines = content.split("\n");
1437
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1438
+ if (headerIdx === -1) {
1439
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1440
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
1441
+ }
1442
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
1443
+ const trimmed = lines[i].trim();
1444
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1445
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1446
+ if (!m) continue;
1447
+ const value = m[4].replace(/\s*#.*$/, "").trim();
1448
+ if (value === "true") return { next: content, changed: false };
1449
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1450
+ return { next: lines.join("\n"), changed: true };
1451
+ }
1452
+ lines.splice(headerIdx + 1, 0, "hooks = true");
1453
+ return { next: lines.join("\n"), changed: true };
1454
+ }
1455
+ function installCodexFeatureFlag(path, dryRun) {
1456
+ const existed = existsSync5(path);
1457
+ const content = existed ? readFileSync3(path, "utf8") : "";
1458
+ const { next, changed } = ensureCodexFeaturesHooks(content);
1459
+ if (!changed) return { path, status: "current" };
1460
+ if (!dryRun) {
1461
+ mkdirSync4(dirname3(path), { recursive: true });
1462
+ writeFileSync4(path, next);
1463
+ }
1464
+ return { path, status: existed ? "merged" : "created" };
1465
+ }
1466
+ function resolveSurfaces(surface, cwd) {
1467
+ if (surface === "claude") return ["claude"];
1468
+ if (surface === "codex") return ["codex"];
1469
+ if (surface === "both") return ["claude", "codex"];
1470
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1471
+ const surfaces = detectHookSurfaces(cwd);
1472
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
1473
+ }
1474
+ function describe(result, dryRun) {
1475
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1476
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1477
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
1478
+ }
1479
+ var HOOK_SURFACE_LABEL = {
1480
+ claude: "Claude Code",
1481
+ codex: "Codex"
1482
+ };
1483
+ function installHookSurfaces(surfaces, opts) {
1484
+ const out = [];
1485
+ for (const surface of surfaces) {
1486
+ if (surface === "claude") {
1487
+ const path = join6(opts.claudeDir, "settings.json");
1488
+ out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1489
+ } else {
1490
+ const hooksJson = installHooksJson(join6(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1491
+ const featureFlag = installCodexFeatureFlag(join6(opts.codexHome, "config.toml"), opts.dryRun);
1492
+ out.push({ surface, results: [hooksJson, featureFlag] });
1493
+ }
1494
+ }
1495
+ return out;
1496
+ }
1497
+ function detectHookSurfaces(cwd) {
1498
+ const detected = detectInstalledClients(cwd);
1499
+ const surfaces = [];
1500
+ if (detected.includes("claude-code")) surfaces.push("claude");
1501
+ if (detected.includes("codex")) surfaces.push("codex");
1502
+ return surfaces;
1503
+ }
1504
+ function isSechroomOnPath() {
1505
+ const pathEnv = process.env.PATH ?? "";
1506
+ if (!pathEnv) return false;
1507
+ const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1508
+ for (const dir of pathEnv.split(delimiter)) {
1509
+ if (!dir) continue;
1510
+ for (const name of names) {
1511
+ if (existsSync5(join6(dir, name))) return true;
1512
+ }
1513
+ }
1514
+ return false;
1515
+ }
1516
+ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1517
+ if (isSechroomOnPath()) return false;
1518
+ write(
1519
+ "\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
1520
+ );
1521
+ return true;
1522
+ }
1523
+
1524
+ // src/commands/channel.ts
1309
1525
  function registerChannel(program2) {
1310
1526
  const channel = program2.command("channel").description(
1311
1527
  "Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
@@ -1328,11 +1544,16 @@ function registerChannel(program2) {
1328
1544
  const cfg = resolveConfig(cmd.optsWithGlobals());
1329
1545
  const filter = readFilter(opts);
1330
1546
  const sub = await ensureSubscription(cfg, opts.name, filter);
1331
- const conn = await openConnection(cfg, (payload) => {
1332
- process.stdout.write(
1547
+ const seen = /* @__PURE__ */ new Set();
1548
+ const deliver = makeDeliver(
1549
+ filter,
1550
+ seen,
1551
+ (payload) => process.stdout.write(
1333
1552
  (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
1334
- );
1335
- });
1553
+ )
1554
+ );
1555
+ const conn = await openConnection(cfg, deliver);
1556
+ await reconcile(cfg, filter, deliver);
1336
1557
  if (json) {
1337
1558
  emit(
1338
1559
  {
@@ -1369,7 +1590,8 @@ function registerChannel(program2) {
1369
1590
  );
1370
1591
  await mcp.connect(new StdioServerTransport());
1371
1592
  await ensureSubscription(cfg, opts.name, filter);
1372
- const conn = await openConnection(cfg, (payload) => {
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ const deliver = makeDeliver(filter, seen, (payload) => {
1373
1595
  const { content, meta } = summarizeEvent(payload);
1374
1596
  void mcp.notification({
1375
1597
  method: "notifications/claude/channel",
@@ -1379,6 +1601,8 @@ function registerChannel(program2) {
1379
1601
  `))
1380
1602
  );
1381
1603
  });
1604
+ const conn = await openConnection(cfg, deliver);
1605
+ await reconcile(cfg, filter, deliver);
1382
1606
  process.stderr.write(
1383
1607
  style.dim(
1384
1608
  `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
@@ -1387,6 +1611,55 @@ function registerChannel(program2) {
1387
1611
  );
1388
1612
  await holdOpen(conn);
1389
1613
  });
1614
+ channel.command("install").description(
1615
+ "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
1616
+ ).option(
1617
+ "--workspace <wsp...>",
1618
+ "Restrict dispatches to these workspace id(s)"
1619
+ ).option(
1620
+ "--tag <tag...>",
1621
+ "Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
1622
+ ["kind:task"]
1623
+ ).option(
1624
+ "--name <name>",
1625
+ "MCP server + subscription name (idempotent per name)",
1626
+ "sechroom-channel"
1627
+ ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
1628
+ const path = join7(process.cwd(), ".mcp.json");
1629
+ const dryRun = Boolean(opts.dryRun);
1630
+ const args = ["channel", "mcp", "--name", opts.name];
1631
+ for (const w of opts.workspace ?? [])
1632
+ args.push("--workspace", w);
1633
+ for (const t of opts.tag ?? []) args.push("--tag", t);
1634
+ const entry = { command: "sechroom", args };
1635
+ const config2 = readMcpConfig(path);
1636
+ config2.mcpServers ??= {};
1637
+ const existing = config2.mcpServers[opts.name];
1638
+ const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
1639
+ if (status !== "current" && !dryRun) {
1640
+ config2.mcpServers[opts.name] = entry;
1641
+ mkdirSync5(dirname4(path), { recursive: true });
1642
+ writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1643
+ }
1644
+ const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
1645
+ process.stdout.write(`${style.green("channel")} ${path} (${verb})
1646
+ `);
1647
+ process.stdout.write(
1648
+ style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
1649
+ `)
1650
+ );
1651
+ if (status !== "current") {
1652
+ process.stdout.write(
1653
+ style.dim(
1654
+ `
1655
+ Load it (Channels research preview) by launching your agent with:
1656
+ claude --dangerously-load-development-channels server:${opts.name}
1657
+ `
1658
+ )
1659
+ );
1660
+ }
1661
+ warnIfSechroomNotOnPath();
1662
+ });
1390
1663
  channel.addHelpText(
1391
1664
  "after",
1392
1665
  `
@@ -1395,11 +1668,23 @@ Examples:
1395
1668
  $ sechroom channel connect --tag kind:task --tag status:in-progress
1396
1669
  $ sechroom channel connect --workspace wsp_X --json | jq .
1397
1670
 
1398
- # As a Claude Code channel (research preview, v2.1.80+):
1399
- # add to .mcp.json: { "mcpServers": { "sechroom": { "command": "npx", "args": ["@sechroom/cli", "channel", "mcp"] } } }
1400
- # then: claude --dangerously-load-development-channels server:sechroom`
1671
+ # Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
1672
+ $ sechroom channel install --workspace wsp_X --tag kind:task --tag status:in-progress
1673
+ # then: claude --dangerously-load-development-channels server:sechroom-channel`
1401
1674
  );
1402
1675
  }
1676
+ function readMcpConfig(path) {
1677
+ if (!existsSync6(path)) return {};
1678
+ const raw = readFileSync4(path, "utf8");
1679
+ if (!raw.trim()) return {};
1680
+ try {
1681
+ return JSON.parse(raw);
1682
+ } catch {
1683
+ return fail(
1684
+ `Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
1685
+ );
1686
+ }
1687
+ }
1403
1688
  function readFilter(opts) {
1404
1689
  const tags = opts.tag ?? [];
1405
1690
  const workspaceScope = opts.workspace ?? [];
@@ -1452,20 +1737,134 @@ function holdOpen(conn) {
1452
1737
  process.on("SIGTERM", stop);
1453
1738
  });
1454
1739
  }
1455
- function summarizeEvent(payload) {
1740
+ function parseEvent(payload) {
1456
1741
  let data = payload;
1457
1742
  if (typeof payload === "string") {
1458
1743
  try {
1459
1744
  data = JSON.parse(payload);
1460
1745
  } catch {
1461
- return { content: payload, meta: {} };
1746
+ return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
1462
1747
  }
1463
1748
  }
1464
1749
  const obj = data ?? {};
1465
1750
  const inner = obj.data ?? obj;
1466
- const eventType = str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event";
1467
- const memoryId = str(inner.memoryId ?? inner.MemoryId);
1468
- const workspaceId = str(inner.workspaceId ?? inner.WorkspaceId);
1751
+ const rawTags = inner.tags ?? inner.Tags;
1752
+ return {
1753
+ eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
1754
+ memoryId: str(inner.memoryId ?? inner.MemoryId),
1755
+ workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
1756
+ tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
1757
+ };
1758
+ }
1759
+ function shouldDeliver(payload, filter) {
1760
+ const { workspaceId, tags } = parseEvent(payload);
1761
+ if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
1762
+ return false;
1763
+ if (filter.tags.length > 0) {
1764
+ if (!tags) return false;
1765
+ return facetedTagMatch(tags, filter.tags);
1766
+ }
1767
+ return true;
1768
+ }
1769
+ function makeDeliver(filter, seen, forward) {
1770
+ return (payload) => {
1771
+ if (!shouldDeliver(payload, filter)) return;
1772
+ const { memoryId } = parseEvent(payload);
1773
+ if (memoryId) {
1774
+ if (seen.has(memoryId)) return;
1775
+ seen.add(memoryId);
1776
+ }
1777
+ forward(payload);
1778
+ };
1779
+ }
1780
+ async function reconcile(cfg, filter, deliver) {
1781
+ if (filter.workspaceScope.length === 0) {
1782
+ process.stderr.write(
1783
+ style.dim(
1784
+ "channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
1785
+ )
1786
+ );
1787
+ return;
1788
+ }
1789
+ if (filter.tags.length === 0) return;
1790
+ let token;
1791
+ try {
1792
+ token = await requireToken(cfg);
1793
+ } catch {
1794
+ return;
1795
+ }
1796
+ const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
1797
+ let recovered = 0;
1798
+ for (const ws of filter.workspaceScope) {
1799
+ try {
1800
+ const resp = await fetch(
1801
+ `${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
1802
+ {
1803
+ headers: {
1804
+ authorization: `Bearer ${token}`,
1805
+ tenant: cfg.tenant,
1806
+ "x-sechroom-surface": "cli"
1807
+ }
1808
+ }
1809
+ );
1810
+ if (!resp.ok) {
1811
+ process.stderr.write(
1812
+ err(
1813
+ `channel: reconcile query for ${ws} failed (HTTP ${resp.status})
1814
+ `
1815
+ )
1816
+ );
1817
+ continue;
1818
+ }
1819
+ const data = await resp.json();
1820
+ for (const m of data.results ?? []) {
1821
+ if (!m.id) continue;
1822
+ deliver({
1823
+ eventType: "reconcile",
1824
+ memoryId: m.id,
1825
+ workspaceId: ws,
1826
+ tags: m.tags ?? []
1827
+ });
1828
+ recovered++;
1829
+ }
1830
+ } catch (e) {
1831
+ process.stderr.write(
1832
+ err(`channel: reconcile error for ${ws}: ${String(e)}
1833
+ `)
1834
+ );
1835
+ }
1836
+ }
1837
+ if (recovered > 0)
1838
+ process.stderr.write(
1839
+ style.dim(
1840
+ `channel: reconciled ${recovered} already-queued event(s) on connect.
1841
+ `
1842
+ )
1843
+ );
1844
+ }
1845
+ function facetedTagMatch(eventTags, filterTags) {
1846
+ const have = new Set(eventTags);
1847
+ const groups = /* @__PURE__ */ new Map();
1848
+ for (const f of filterTags) {
1849
+ const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
1850
+ const group = groups.get(ns) ?? [];
1851
+ group.push(f);
1852
+ groups.set(ns, group);
1853
+ }
1854
+ for (const [ns, group] of groups) {
1855
+ const ok2 = group.some(
1856
+ (f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
1857
+ );
1858
+ if (!ok2) return false;
1859
+ }
1860
+ return true;
1861
+ }
1862
+ function namespaceOf(tag) {
1863
+ const i = tag.indexOf(":");
1864
+ return i >= 0 ? tag.slice(0, i) : tag;
1865
+ }
1866
+ function summarizeEvent(payload) {
1867
+ const { eventType, memoryId, workspaceId } = parseEvent(payload);
1469
1868
  const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
1470
1869
  const meta = {};
1471
1870
  if (eventType) meta.event_type = eventType;
@@ -1560,28 +1959,28 @@ Examples:
1560
1959
  }
1561
1960
 
1562
1961
  // src/commands/checkpoint.ts
1563
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1564
- import { dirname as dirname6, join as join9 } from "path";
1962
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
1963
+ import { dirname as dirname7, join as join10 } from "path";
1565
1964
 
1566
1965
  // src/commands/hook.ts
1567
1966
  import { createHash as createHash2 } from "crypto";
1568
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
1569
- import { dirname as dirname5, join as join8 } from "path";
1967
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
1968
+ import { dirname as dirname6, join as join9 } from "path";
1570
1969
 
1571
1970
  // src/sem.ts
1572
- import { dirname as dirname2, join as join5 } from "path";
1573
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1574
- var SEM_FILE = join5(".sechroom", "lane.json");
1971
+ import { dirname as dirname5, join as join8 } from "path";
1972
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
1973
+ var SEM_FILE = join8(".sechroom", "lane.json");
1575
1974
  var STATE_DIR_NAME2 = ".sechroom";
1576
1975
  function localSemPath(cwd = process.cwd()) {
1577
- return join5(cwd, SEM_FILE);
1976
+ return join8(cwd, SEM_FILE);
1578
1977
  }
1579
1978
  function resolveSemPathForRead(start = process.cwd()) {
1580
1979
  let dir = start;
1581
1980
  while (true) {
1582
- const candidate = join5(dir, SEM_FILE);
1583
- if (existsSync4(candidate)) return candidate;
1584
- const parent = dirname2(dir);
1981
+ const candidate = join8(dir, SEM_FILE);
1982
+ if (existsSync7(candidate)) return candidate;
1983
+ const parent = dirname5(dir);
1585
1984
  if (parent === dir) return void 0;
1586
1985
  dir = parent;
1587
1986
  }
@@ -1591,23 +1990,23 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1591
1990
  let dir = start;
1592
1991
  let gitPath;
1593
1992
  for (; ; ) {
1594
- const candidate = join5(dir, ".git");
1595
- if (existsSync4(candidate)) {
1993
+ const candidate = join8(dir, ".git");
1994
+ if (existsSync7(candidate)) {
1596
1995
  gitPath = candidate;
1597
1996
  break;
1598
1997
  }
1599
- const parent = dirname2(dir);
1998
+ const parent = dirname5(dir);
1600
1999
  if (parent === dir) break;
1601
2000
  dir = parent;
1602
2001
  }
1603
2002
  if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1604
- const gitFile = readFileSync3(gitPath, "utf8");
2003
+ const gitFile = readFileSync5(gitPath, "utf8");
1605
2004
  const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1606
2005
  if (!common) return lane;
1607
- const worktreesDir = join5(common[1], "worktrees");
2006
+ const worktreesDir = join8(common[1], "worktrees");
1608
2007
  const siblings = readdirSync(worktreesDir).filter((n) => {
1609
2008
  try {
1610
- return statSync(join5(worktreesDir, n)).isDirectory();
2009
+ return statSync(join8(worktreesDir, n)).isDirectory();
1611
2010
  } catch {
1612
2011
  return false;
1613
2012
  }
@@ -1617,329 +2016,117 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1617
2016
  return lane;
1618
2017
  }
1619
2018
  }
1620
- function laneWithWorktreeSuffix(lane, gitFile, siblings) {
1621
- const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
1622
- if (!m) return lane;
1623
- const idx = [...siblings].sort().indexOf(m[1]);
1624
- return idx < 0 ? lane : `${lane}-${idx + 2}`;
1625
- }
1626
- function serializeSem(values) {
1627
- return JSON.stringify(values, null, 2) + "\n";
1628
- }
1629
- function readSem(path) {
1630
- const p = path ?? resolveSemPathForRead();
1631
- if (!p || !existsSync4(p)) return void 0;
1632
- return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
1633
- }
1634
- function readLocalSemValues(cwd = process.cwd()) {
1635
- const next = join5(cwd, SEM_FILE);
1636
- if (existsSync4(next)) return readSem(next)?.values ?? {};
1637
- return {};
1638
- }
1639
- function parseLaneJson(text2) {
1640
- try {
1641
- const parsed = JSON.parse(text2);
1642
- const out = {};
1643
- for (const [k, v] of Object.entries(parsed)) {
1644
- if (typeof v === "string") out[k] = v;
1645
- }
1646
- return out;
1647
- } catch {
1648
- return {};
1649
- }
1650
- }
1651
- var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1652
- function writeSem(values, path = localSemPath()) {
1653
- mkdirSync4(dirname2(path), { recursive: true });
1654
- writeFileSync4(path, serializeSem(values));
1655
- ensureSemIgnored(path);
1656
- ensureContinuityScaffold(path);
1657
- return path;
1658
- }
1659
- function ensureStateDirIgnored(cwd = process.cwd()) {
1660
- ensureSemIgnored(localSemPath(cwd));
1661
- }
1662
- var CONTINUITY_FILE_NAME = "continuity.json";
1663
- var CONTINUITY_SCAFFOLD = JSON.stringify(
1664
- {
1665
- _readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
1666
- objective: "",
1667
- state: "",
1668
- lastAction: "",
1669
- nextAction: "",
1670
- resumeInstruction: "",
1671
- constraints: [],
1672
- questions: [],
1673
- artifacts: [],
1674
- confidence: null
1675
- },
1676
- null,
1677
- 2
1678
- ) + "\n";
1679
- function ensureContinuityScaffold(semPath) {
1680
- try {
1681
- const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
1682
- if (existsSync4(target)) return;
1683
- writeFileSync4(target, CONTINUITY_SCAFFOLD);
1684
- } catch {
1685
- }
1686
- }
1687
- function ignoresSem(content) {
1688
- return content.split("\n").some((line) => {
1689
- const t = line.trim();
1690
- return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
1691
- });
1692
- }
1693
- function inGitRepo(startDir) {
1694
- let dir = startDir;
1695
- for (; ; ) {
1696
- if (existsSync4(join5(dir, ".git"))) return true;
1697
- const parent = dirname2(dir);
1698
- if (parent === dir) return false;
1699
- dir = parent;
1700
- }
1701
- }
1702
- function resolveGitignoreTarget(startDir) {
1703
- let dir = startDir;
1704
- for (; ; ) {
1705
- const gi = join5(dir, ".gitignore");
1706
- if (existsSync4(gi)) return { path: gi, exists: true };
1707
- const parent = dirname2(dir);
1708
- if (existsSync4(join5(dir, ".git")) || parent === dir) {
1709
- return { path: join5(startDir, ".gitignore"), exists: false };
1710
- }
1711
- dir = parent;
1712
- }
1713
- }
1714
- function ensureSemIgnored(semPath) {
1715
- try {
1716
- const checkoutDir = dirname2(dirname2(semPath));
1717
- if (!inGitRepo(checkoutDir)) return;
1718
- const target = resolveGitignoreTarget(checkoutDir);
1719
- if (target.exists) {
1720
- const content = readFileSync3(target.path, "utf8");
1721
- if (ignoresSem(content)) return;
1722
- const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1723
- appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1724
- `);
1725
- } else {
1726
- writeFileSync4(target.path, `${STATE_DIR_IGNORE}
1727
- `);
1728
- }
1729
- } catch {
1730
- }
1731
- }
1732
-
1733
- // src/commands/hook-install.ts
1734
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1735
- import { delimiter, dirname as dirname4, join as join7 } from "path";
1736
-
1737
- // src/setup/clients.ts
1738
- import { existsSync as existsSync5 } from "fs";
1739
- import { homedir as homedir3 } from "os";
1740
- import { dirname as dirname3, join as join6 } from "path";
1741
- function claudeDesktopConfigPath(home) {
1742
- switch (process.platform) {
1743
- case "darwin":
1744
- return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1745
- case "win32":
1746
- return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1747
- default:
1748
- return join6(home, ".config", "Claude", "claude_desktop_config.json");
1749
- }
1750
- }
1751
- function clientTargets(cwd, opts = {}) {
1752
- const home = homedir3();
1753
- const claudeDir = opts.claudeDir ?? join6(home, ".claude");
1754
- const codexHome = opts.codexHome ?? join6(home, ".codex");
1755
- return {
1756
- "claude-code": {
1757
- key: "claude-code",
1758
- label: "Claude Code",
1759
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
1760
- instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
1761
- },
1762
- "claude-desktop": {
1763
- key: "claude-desktop",
1764
- label: "Claude Desktop",
1765
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1766
- instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
1767
- },
1768
- codex: {
1769
- key: "codex",
1770
- label: "Codex CLI",
1771
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
1772
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1773
- },
1774
- cursor: {
1775
- key: "cursor",
1776
- label: "Cursor",
1777
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
1778
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1779
- },
1780
- antigravity: {
1781
- key: "antigravity",
1782
- label: "Google Antigravity",
1783
- // FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
1784
- // `~/.gemini/config/mcp_config.json` (not cwd; not affected by
1785
- // CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
1786
- // `type` — comes from the `antigravity` server surface, so we don't
1787
- // hardcode it here. Instructions go in the project `AGENTS.md`
1788
- // (cross-tool, shared with Codex/Cursor).
1789
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1790
- instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
1791
- }
1792
- };
1793
- }
1794
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
1795
- var DEFAULT_CLIENT_KEY = "claude-code";
1796
- function detectInstalledClients(cwd) {
1797
- const home = homedir3();
1798
- const detected = [];
1799
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
1800
- if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1801
- if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
1802
- if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
1803
- if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
1804
- return detected;
1805
- }
1806
-
1807
- // src/commands/hook-install.ts
1808
- var CLAUDE_HOOK_COMMANDS = {
1809
- SessionStart: "sechroom hook session-start",
1810
- PreCompact: "sechroom hook pre-compact",
1811
- SessionEnd: "sechroom hook session-end",
1812
- // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1813
- // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1814
- // for every Claude install. Claude-only (it parses a Claude Code transcript).
1815
- Stop: "sechroom telemetry hook"
1816
- };
1817
- var CODEX_HOOK_COMMANDS = {
1818
- SessionStart: "sechroom hook session-start",
1819
- Stop: "sechroom hook session-end --debounce-minutes 10"
1820
- };
1821
- function hasHookCommand(config2, event, command) {
1822
- const groups = config2.hooks?.[event] ?? [];
1823
- return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1824
- }
1825
- function mergeHooks(config2, commands) {
1826
- config2.hooks ??= {};
1827
- let added = 0;
1828
- for (const [event, command] of Object.entries(commands)) {
1829
- if (hasHookCommand(config2, event, command)) continue;
1830
- const groups = config2.hooks[event] ??= [];
1831
- groups.push({ hooks: [{ type: "command", command }] });
1832
- added += 1;
1833
- }
1834
- return added;
2019
+ function laneWithWorktreeSuffix(lane, gitFile, siblings) {
2020
+ const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
2021
+ if (!m) return lane;
2022
+ const idx = [...siblings].sort().indexOf(m[1]);
2023
+ return idx < 0 ? lane : `${lane}-${idx + 2}`;
1835
2024
  }
1836
- function readJsonConfig2(path) {
1837
- if (!existsSync6(path)) return {};
1838
- const raw = readFileSync4(path, "utf8");
1839
- if (!raw.trim()) return {};
1840
- return JSON.parse(raw);
2025
+ function serializeSem(values) {
2026
+ return JSON.stringify(values, null, 2) + "\n";
1841
2027
  }
1842
- function installHooksJson(path, commands, dryRun) {
1843
- const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
1844
- const config2 = readJsonConfig2(path);
1845
- const added = mergeHooks(config2, commands);
1846
- if (added === 0 && existed) return { path, status: "current" };
1847
- if (!dryRun) {
1848
- mkdirSync5(dirname4(path), { recursive: true });
1849
- writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1850
- }
1851
- return { path, status: existed ? "merged" : "created" };
2028
+ function readSem(path) {
2029
+ const p = path ?? resolveSemPathForRead();
2030
+ if (!p || !existsSync7(p)) return void 0;
2031
+ return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
1852
2032
  }
1853
- function installClaudeCommands(claudeDir, commands, dryRun) {
1854
- return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
2033
+ function readLocalSemValues(cwd = process.cwd()) {
2034
+ const next = join8(cwd, SEM_FILE);
2035
+ if (existsSync7(next)) return readSem(next)?.values ?? {};
2036
+ return {};
1855
2037
  }
1856
- function ensureCodexFeaturesHooks(content) {
1857
- const lines = content.split("\n");
1858
- const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1859
- if (headerIdx === -1) {
1860
- const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1861
- return { next: base + "\n[features]\nhooks = true\n", changed: true };
1862
- }
1863
- for (let i = headerIdx + 1; i < lines.length; i += 1) {
1864
- const trimmed = lines[i].trim();
1865
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1866
- const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1867
- if (!m) continue;
1868
- const value = m[4].replace(/\s*#.*$/, "").trim();
1869
- if (value === "true") return { next: content, changed: false };
1870
- lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1871
- return { next: lines.join("\n"), changed: true };
2038
+ function parseLaneJson(text2) {
2039
+ try {
2040
+ const parsed = JSON.parse(text2);
2041
+ const out = {};
2042
+ for (const [k, v] of Object.entries(parsed)) {
2043
+ if (typeof v === "string") out[k] = v;
2044
+ }
2045
+ return out;
2046
+ } catch {
2047
+ return {};
1872
2048
  }
1873
- lines.splice(headerIdx + 1, 0, "hooks = true");
1874
- return { next: lines.join("\n"), changed: true };
1875
2049
  }
1876
- function installCodexFeatureFlag(path, dryRun) {
1877
- const existed = existsSync6(path);
1878
- const content = existed ? readFileSync4(path, "utf8") : "";
1879
- const { next, changed } = ensureCodexFeaturesHooks(content);
1880
- if (!changed) return { path, status: "current" };
1881
- if (!dryRun) {
1882
- mkdirSync5(dirname4(path), { recursive: true });
1883
- writeFileSync5(path, next);
2050
+ var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
2051
+ function writeSem(values, path = localSemPath()) {
2052
+ mkdirSync6(dirname5(path), { recursive: true });
2053
+ writeFileSync6(path, serializeSem(values));
2054
+ ensureSemIgnored(path);
2055
+ ensureContinuityScaffold(path);
2056
+ return path;
2057
+ }
2058
+ function ensureStateDirIgnored(cwd = process.cwd()) {
2059
+ ensureSemIgnored(localSemPath(cwd));
2060
+ }
2061
+ var CONTINUITY_FILE_NAME = "continuity.json";
2062
+ var CONTINUITY_SCAFFOLD = JSON.stringify(
2063
+ {
2064
+ _readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
2065
+ objective: "",
2066
+ state: "",
2067
+ lastAction: "",
2068
+ nextAction: "",
2069
+ resumeInstruction: "",
2070
+ constraints: [],
2071
+ questions: [],
2072
+ artifacts: [],
2073
+ confidence: null
2074
+ },
2075
+ null,
2076
+ 2
2077
+ ) + "\n";
2078
+ function ensureContinuityScaffold(semPath) {
2079
+ try {
2080
+ const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
2081
+ if (existsSync7(target)) return;
2082
+ writeFileSync6(target, CONTINUITY_SCAFFOLD);
2083
+ } catch {
1884
2084
  }
1885
- return { path, status: existed ? "merged" : "created" };
1886
2085
  }
1887
- function resolveSurfaces(surface, cwd) {
1888
- if (surface === "claude") return ["claude"];
1889
- if (surface === "codex") return ["codex"];
1890
- if (surface === "both") return ["claude", "codex"];
1891
- if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1892
- const surfaces = detectHookSurfaces(cwd);
1893
- return surfaces.length > 0 ? surfaces : ["claude", "codex"];
2086
+ function ignoresSem(content) {
2087
+ return content.split("\n").some((line) => {
2088
+ const t = line.trim();
2089
+ return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
2090
+ });
1894
2091
  }
1895
- function describe(result, dryRun) {
1896
- if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1897
- const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1898
- return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
2092
+ function inGitRepo(startDir) {
2093
+ let dir = startDir;
2094
+ for (; ; ) {
2095
+ if (existsSync7(join8(dir, ".git"))) return true;
2096
+ const parent = dirname5(dir);
2097
+ if (parent === dir) return false;
2098
+ dir = parent;
2099
+ }
1899
2100
  }
1900
- var HOOK_SURFACE_LABEL = {
1901
- claude: "Claude Code",
1902
- codex: "Codex"
1903
- };
1904
- function installHookSurfaces(surfaces, opts) {
1905
- const out = [];
1906
- for (const surface of surfaces) {
1907
- if (surface === "claude") {
1908
- const path = join7(opts.claudeDir, "settings.json");
1909
- out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1910
- } else {
1911
- const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1912
- const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
1913
- out.push({ surface, results: [hooksJson, featureFlag] });
2101
+ function resolveGitignoreTarget(startDir) {
2102
+ let dir = startDir;
2103
+ for (; ; ) {
2104
+ const gi = join8(dir, ".gitignore");
2105
+ if (existsSync7(gi)) return { path: gi, exists: true };
2106
+ const parent = dirname5(dir);
2107
+ if (existsSync7(join8(dir, ".git")) || parent === dir) {
2108
+ return { path: join8(startDir, ".gitignore"), exists: false };
1914
2109
  }
2110
+ dir = parent;
1915
2111
  }
1916
- return out;
1917
- }
1918
- function detectHookSurfaces(cwd) {
1919
- const detected = detectInstalledClients(cwd);
1920
- const surfaces = [];
1921
- if (detected.includes("claude-code")) surfaces.push("claude");
1922
- if (detected.includes("codex")) surfaces.push("codex");
1923
- return surfaces;
1924
2112
  }
1925
- function isSechroomOnPath() {
1926
- const pathEnv = process.env.PATH ?? "";
1927
- if (!pathEnv) return false;
1928
- const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1929
- for (const dir of pathEnv.split(delimiter)) {
1930
- if (!dir) continue;
1931
- for (const name of names) {
1932
- if (existsSync6(join7(dir, name))) return true;
2113
+ function ensureSemIgnored(semPath) {
2114
+ try {
2115
+ const checkoutDir = dirname5(dirname5(semPath));
2116
+ if (!inGitRepo(checkoutDir)) return;
2117
+ const target = resolveGitignoreTarget(checkoutDir);
2118
+ if (target.exists) {
2119
+ const content = readFileSync5(target.path, "utf8");
2120
+ if (ignoresSem(content)) return;
2121
+ const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
2122
+ appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
2123
+ `);
2124
+ } else {
2125
+ writeFileSync6(target.path, `${STATE_DIR_IGNORE}
2126
+ `);
1933
2127
  }
2128
+ } catch {
1934
2129
  }
1935
- return false;
1936
- }
1937
- function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1938
- if (isSechroomOnPath()) return false;
1939
- write(
1940
- "\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
1941
- );
1942
- return true;
1943
2130
  }
1944
2131
 
1945
2132
  // src/commands/hook.ts
@@ -1966,13 +2153,13 @@ function resolveLane(flagLane, cwd) {
1966
2153
  if (!base) return void 0;
1967
2154
  return applyWorktreeLaneSuffix(base, start);
1968
2155
  }
1969
- var INTENT_FILE = join8(".sechroom", "continuity.json");
2156
+ var INTENT_FILE = join9(".sechroom", "continuity.json");
1970
2157
  function resolveIntentPath(start) {
1971
2158
  let dir = start;
1972
2159
  for (; ; ) {
1973
- const candidate = join8(dir, INTENT_FILE);
1974
- if (existsSync7(candidate)) return candidate;
1975
- const parent = dirname5(dir);
2160
+ const candidate = join9(dir, INTENT_FILE);
2161
+ if (existsSync8(candidate)) return candidate;
2162
+ const parent = dirname6(dir);
1976
2163
  if (parent === dir) return void 0;
1977
2164
  dir = parent;
1978
2165
  }
@@ -1981,7 +2168,7 @@ function readIntent(start) {
1981
2168
  const path = resolveIntentPath(start);
1982
2169
  if (!path) return void 0;
1983
2170
  try {
1984
- return JSON.parse(readFileSync5(path, "utf8"));
2171
+ return JSON.parse(readFileSync6(path, "utf8"));
1985
2172
  } catch {
1986
2173
  return void 0;
1987
2174
  }
@@ -2023,14 +2210,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
2023
2210
  }
2024
2211
  function ledgerPath(start) {
2025
2212
  const intent = resolveIntentPath(start);
2026
- const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
2027
- return join8(dir, ".checkpoint-state.json");
2213
+ const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
2214
+ return join9(dir, ".checkpoint-state.json");
2028
2215
  }
2029
2216
  function readLedger(start) {
2030
2217
  try {
2031
2218
  const p = ledgerPath(start);
2032
- if (!existsSync7(p)) return {};
2033
- return JSON.parse(readFileSync5(p, "utf8"));
2219
+ if (!existsSync8(p)) return {};
2220
+ return JSON.parse(readFileSync6(p, "utf8"));
2034
2221
  } catch {
2035
2222
  return {};
2036
2223
  }
@@ -2077,13 +2264,13 @@ function recordPush(start, intent) {
2077
2264
  } catch {
2078
2265
  mtimeMs = void 0;
2079
2266
  }
2080
- mkdirSync6(dirname5(p), { recursive: true });
2267
+ mkdirSync7(dirname6(p), { recursive: true });
2081
2268
  const ledger = {
2082
2269
  lastEpochMs: Date.now(),
2083
2270
  lastMtimeMs: mtimeMs,
2084
2271
  lastHash: intentHash(intent)
2085
2272
  };
2086
- writeFileSync6(p, JSON.stringify(ledger) + "\n");
2273
+ writeFileSync7(p, JSON.stringify(ledger) + "\n");
2087
2274
  } catch {
2088
2275
  }
2089
2276
  }
@@ -2330,10 +2517,10 @@ Examples:
2330
2517
  const client = await makeClient(cfg);
2331
2518
  return client.POST("/continuity/snapshots", { body });
2332
2519
  });
2333
- const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
2520
+ const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
2334
2521
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2335
- mkdirSync7(dirname6(path), { recursive: true });
2336
- writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
2522
+ mkdirSync8(dirname7(path), { recursive: true });
2523
+ writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
2337
2524
  recordPush(cwd, merged);
2338
2525
  if (json) {
2339
2526
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -2346,6 +2533,149 @@ Examples:
2346
2533
  });
2347
2534
  }
2348
2535
 
2536
+ // src/commands/close.ts
2537
+ import { readFileSync as readFileSync7 } from "fs";
2538
+ var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
2539
+ function registerClose(program2) {
2540
+ program2.command("close").description(
2541
+ "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)."
2542
+ ).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
2543
+ "--workspace <wsp>",
2544
+ "Workspace that owns the closeout memo"
2545
+ ).requiredOption("--title <title>", "Closeout title").option(
2546
+ "--file <path>",
2547
+ "Read the closeout body from a file (default: stdin)"
2548
+ ).option(
2549
+ "--decomposition <id>",
2550
+ "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."
2551
+ ).option(
2552
+ "--no-status-flip",
2553
+ "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.)"
2554
+ ).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
2555
+ "after",
2556
+ `
2557
+ Examples:
2558
+ # Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
2559
+ $ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
2560
+ --verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
2561
+
2562
+ # Bare task:
2563
+ $ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
2564
+ --title "done" --file ./closeout.md`
2565
+ ).action(async (opts, cmd) => {
2566
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2567
+ const json = cmd.optsWithGlobals().json;
2568
+ const verdict = String(opts.verdict);
2569
+ if (!VERDICTS.includes(verdict))
2570
+ fail(
2571
+ `--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
2572
+ );
2573
+ let bodyText;
2574
+ try {
2575
+ bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
2576
+ } catch {
2577
+ fail(
2578
+ opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
2579
+ );
2580
+ }
2581
+ if (!bodyText.trim())
2582
+ fail(
2583
+ "closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
2584
+ );
2585
+ const client = await makeClient(cfg);
2586
+ let matchTags;
2587
+ if (opts.decomposition) {
2588
+ const run = await runApi(
2589
+ "Reading run contract",
2590
+ async () => client.GET("/decompositions/{id}/run", {
2591
+ params: { path: { id: opts.decomposition } }
2592
+ })
2593
+ );
2594
+ const await_ = run.awaitingCompletion;
2595
+ if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
2596
+ fail(
2597
+ `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.`
2598
+ );
2599
+ if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
2600
+ fail(
2601
+ `run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
2602
+ );
2603
+ matchTags = await_.matchTags;
2604
+ } else {
2605
+ matchTags = [`wlp-task:${opts.task}`];
2606
+ }
2607
+ const tags = [
2608
+ .../* @__PURE__ */ new Set([
2609
+ ...matchTags,
2610
+ `wlp-task:${opts.task}`,
2611
+ `verdict:${verdict}`
2612
+ ])
2613
+ ];
2614
+ const closeout = await runApi(
2615
+ "Creating closeout",
2616
+ async () => client.POST("/memories", {
2617
+ body: {
2618
+ text: bodyText,
2619
+ type: "reference",
2620
+ content: "{}",
2621
+ confidence: 1,
2622
+ source: opts.source,
2623
+ archetype: "Document",
2624
+ title: opts.title,
2625
+ tags,
2626
+ owner: { type: "Workspace", id: opts.workspace }
2627
+ }
2628
+ })
2629
+ );
2630
+ const edge = await runApi(
2631
+ "Wiring Reference edge",
2632
+ async () => client.POST("/memories/{memoryId}/relationships", {
2633
+ params: { path: { memoryId: closeout.id } },
2634
+ body: { toMemoryId: opts.task, type: "Reference" }
2635
+ })
2636
+ );
2637
+ let taskStatus;
2638
+ if (opts.statusFlip !== false && !opts.decomposition) {
2639
+ const current = await runApi(
2640
+ "Reading task tags",
2641
+ async () => client.GET("/memories/{memoryId}", {
2642
+ params: { path: { memoryId: opts.task } }
2643
+ })
2644
+ );
2645
+ const currentTags = current?.item?.tags ?? current?.tags ?? [];
2646
+ const target = verdict === "blocked" ? "status:blocked" : "status:done";
2647
+ const nextTags = [
2648
+ ...new Set(currentTags.filter((t) => !t.startsWith("status:")))
2649
+ ].concat(target);
2650
+ await runApi(
2651
+ "Flipping task status",
2652
+ async () => client.PATCH("/memories/{memoryId}/metadata", {
2653
+ params: { path: { memoryId: opts.task } },
2654
+ body: {
2655
+ memoryId: opts.task,
2656
+ source: opts.source,
2657
+ tags: nextTags
2658
+ }
2659
+ })
2660
+ );
2661
+ taskStatus = target.slice("status:".length);
2662
+ }
2663
+ const view = resolveViewUrl(cfg.baseUrl, closeout.url);
2664
+ const result = {
2665
+ closeoutId: closeout.id,
2666
+ edgeId: edge.id,
2667
+ taskStatus: taskStatus ?? null,
2668
+ verdict,
2669
+ tags
2670
+ };
2671
+ emitAction(
2672
+ `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}` : ""}`,
2673
+ result,
2674
+ json
2675
+ );
2676
+ });
2677
+ }
2678
+
2349
2679
  // src/commands/continuity.ts
2350
2680
  function registerContinuity(program2) {
2351
2681
  const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
@@ -2506,6 +2836,90 @@ Examples:
2506
2836
  });
2507
2837
  }
2508
2838
 
2839
+ // src/commands/decomposition.ts
2840
+ function registerDecomposition(program2) {
2841
+ const decomposition = program2.command("decomposition").description(
2842
+ "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
2843
+ );
2844
+ decomposition.addHelpText(
2845
+ "after",
2846
+ `
2847
+ Examples:
2848
+ $ sechroom decomposition decompose mem_XXXX
2849
+ $ sechroom decomposition execute sug_XXXX
2850
+ $ sechroom decomposition accept sug_XXXX
2851
+ $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
2852
+ );
2853
+ decomposition.command("decompose <briefId>").description(
2854
+ "Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
2855
+ ).action(async (briefId, _opts, cmd) => {
2856
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2857
+ const data = await runApi("Queueing decomposition", async () => {
2858
+ const client = await makeClient(cfg);
2859
+ return client.POST("/work-briefs/{id}/decompose", {
2860
+ params: { path: { id: briefId } },
2861
+ body: { id: briefId }
2862
+ });
2863
+ });
2864
+ emitAction(
2865
+ `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
2866
+ data,
2867
+ cmd.optsWithGlobals().json
2868
+ );
2869
+ });
2870
+ decomposition.command("execute <decompositionId>").description(
2871
+ "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
2872
+ ).action(async (decompositionId, _opts, cmd) => {
2873
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2874
+ const data = await runApi("Executing decomposition", async () => {
2875
+ const client = await makeClient(cfg);
2876
+ return client.POST("/decompositions/{id}/execute", {
2877
+ params: { path: { id: decompositionId } },
2878
+ body: {}
2879
+ });
2880
+ });
2881
+ emitAction(
2882
+ `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
2883
+ data,
2884
+ cmd.optsWithGlobals().json
2885
+ );
2886
+ });
2887
+ decomposition.command("accept <decompositionId>").description(
2888
+ "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
2889
+ ).action(async (decompositionId, _opts, cmd) => {
2890
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2891
+ const data = await runApi("Accepting decomposition", async () => {
2892
+ const client = await makeClient(cfg);
2893
+ return client.POST("/decompositions/{id}/accept", {
2894
+ params: { path: { id: decompositionId } },
2895
+ body: {}
2896
+ });
2897
+ });
2898
+ emitAction(
2899
+ `accepted decomposition ${style.bold(decompositionId)}`,
2900
+ data,
2901
+ cmd.optsWithGlobals().json
2902
+ );
2903
+ });
2904
+ decomposition.command("reject <decompositionId>").description(
2905
+ "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
2906
+ ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
2907
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2908
+ const data = await runApi("Rejecting decomposition", async () => {
2909
+ const client = await makeClient(cfg);
2910
+ return client.POST("/decompositions/{id}/reject", {
2911
+ params: { path: { id: decompositionId } },
2912
+ body: { reasonText: opts.reason ?? null }
2913
+ });
2914
+ });
2915
+ emitAction(
2916
+ `rejected decomposition ${style.bold(decompositionId)}`,
2917
+ data,
2918
+ cmd.optsWithGlobals().json
2919
+ );
2920
+ });
2921
+ }
2922
+
2509
2923
  // src/commands/filing.ts
2510
2924
  function registerFiling(program2) {
2511
2925
  const filing = program2.command("filing").description("Review and act on filing suggestions");
@@ -3017,8 +3431,8 @@ Examples:
3017
3431
 
3018
3432
  // src/setup/apply.ts
3019
3433
  import { createHash as createHash3 } from "crypto";
3020
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
3021
- import { dirname as dirname7 } from "path";
3434
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3435
+ import { dirname as dirname8 } from "path";
3022
3436
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3023
3437
  var MARKER_END = "<!-- @sechroom/cli:end";
3024
3438
  function normalizeBody(s) {
@@ -3071,22 +3485,22 @@ function parseManagedBlock(content, block) {
3071
3485
  return null;
3072
3486
  }
3073
3487
  function ensureDir2(path) {
3074
- mkdirSync8(dirname7(path), { recursive: true });
3488
+ mkdirSync9(dirname8(path), { recursive: true });
3075
3489
  }
3076
3490
  function readOr(path, fallback) {
3077
3491
  try {
3078
- return readFileSync6(path, "utf8");
3492
+ return readFileSync8(path, "utf8");
3079
3493
  } catch {
3080
3494
  return fallback;
3081
3495
  }
3082
3496
  }
3083
3497
  function mergeMcpJson(path, snippet, dryRun) {
3084
3498
  const incoming = JSON.parse(snippet);
3085
- const existed = existsSync8(path);
3499
+ const existed = existsSync9(path);
3086
3500
  let current = {};
3087
3501
  if (existed) {
3088
3502
  try {
3089
- current = JSON.parse(readFileSync6(path, "utf8"));
3503
+ current = JSON.parse(readFileSync8(path, "utf8"));
3090
3504
  } catch {
3091
3505
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3092
3506
  }
@@ -3094,26 +3508,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3094
3508
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3095
3509
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3096
3510
  ensureDir2(path);
3097
- writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3511
+ writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3098
3512
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3099
3513
  }
3100
3514
  function mergeCodexToml(path, snippet, dryRun) {
3101
- const existed = existsSync8(path);
3515
+ const existed = existsSync9(path);
3102
3516
  let body = readOr(path, "");
3103
3517
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3104
3518
  const trimmed = body.trim();
3105
3519
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3106
3520
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3107
3521
  ensureDir2(path);
3108
- writeFileSync8(path, next, { mode: 384 });
3522
+ writeFileSync9(path, next, { mode: 384 });
3109
3523
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3110
3524
  }
3111
3525
  function writeInstructionBlock(path, write, dryRun) {
3112
- const existed = existsSync8(path);
3526
+ const existed = existsSync9(path);
3113
3527
  const next = computeBlockFile(readOr(path, ""), write);
3114
3528
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3115
3529
  ensureDir2(path);
3116
- writeFileSync8(path, next);
3530
+ writeFileSync9(path, next);
3117
3531
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3118
3532
  }
3119
3533
  function computeBlockFile(current, write) {
@@ -3154,7 +3568,7 @@ function applyBlock(path, write, mode, dryRun) {
3154
3568
  const next = computeBlockFile(current, write);
3155
3569
  if (!dryRun) {
3156
3570
  ensureDir2(proposedPath);
3157
- writeFileSync8(proposedPath, next);
3571
+ writeFileSync9(proposedPath, next);
3158
3572
  }
3159
3573
  return {
3160
3574
  kind: "instruction",
@@ -3284,8 +3698,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3284
3698
  }
3285
3699
 
3286
3700
  // src/setup/skills-offer.ts
3287
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
3288
- import { join as join10 } from "path";
3701
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
3702
+ import { join as join11 } from "path";
3289
3703
 
3290
3704
  // src/setup/lane-pin.ts
3291
3705
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3401,8 +3815,8 @@ Found ${summary} available to you for ${surface}.
3401
3815
  if (skills.length > 0) {
3402
3816
  const written = [];
3403
3817
  for (const s of skills) {
3404
- mkdirSync9(join10(sDir, s.name), { recursive: true });
3405
- writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3818
+ mkdirSync10(join11(sDir, s.name), { recursive: true });
3819
+ writeFileSync10(join11(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3406
3820
  written.push(s.name);
3407
3821
  }
3408
3822
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3410,11 +3824,11 @@ Found ${summary} available to you for ${surface}.
3410
3824
  `);
3411
3825
  }
3412
3826
  if (agents.length > 0) {
3413
- mkdirSync9(aDir, { recursive: true });
3827
+ mkdirSync10(aDir, { recursive: true });
3414
3828
  const written = [];
3415
3829
  for (const a of agents) {
3416
3830
  const file = `${a.name}.md`;
3417
- writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3831
+ writeFileSync10(join11(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3418
3832
  written.push(file);
3419
3833
  }
3420
3834
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3797,13 +4211,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3797
4211
  }
3798
4212
 
3799
4213
  // src/commands/onboard.ts
3800
- import { existsSync as existsSync10 } from "fs";
3801
- import { basename as basename2, join as join12 } from "path";
4214
+ import { existsSync as existsSync11 } from "fs";
4215
+ import { basename as basename2, join as join13 } from "path";
3802
4216
 
3803
4217
  // src/commands/fanout.ts
3804
4218
  import { spawnSync } from "child_process";
3805
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3806
- import { isAbsolute, join as join11, resolve } from "path";
4219
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
4220
+ import { isAbsolute, join as join12, resolve } from "path";
3807
4221
  var ICON = {
3808
4222
  refresh: "\u21BB",
3809
4223
  bind: "+",
@@ -3823,21 +4237,21 @@ function discoverChildren(root) {
3823
4237
  const out = [];
3824
4238
  for (const name of names.sort()) {
3825
4239
  if (name.startsWith(".") || name === "node_modules") continue;
3826
- const dir = join11(root, name);
4240
+ const dir = join12(root, name);
3827
4241
  try {
3828
4242
  if (!statSync3(dir).isDirectory()) continue;
3829
4243
  } catch {
3830
4244
  continue;
3831
4245
  }
3832
- if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
4246
+ if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3833
4247
  }
3834
4248
  return out;
3835
4249
  }
3836
4250
  function readManifest(path) {
3837
- if (!existsSync9(path)) return null;
4251
+ if (!existsSync10(path)) return null;
3838
4252
  let parsed;
3839
4253
  try {
3840
- parsed = JSON.parse(readFileSync7(path, "utf8"));
4254
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
3841
4255
  } catch (err2) {
3842
4256
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3843
4257
  }
@@ -4203,10 +4617,10 @@ async function chooseScope(scopeFlag, yes) {
4203
4617
  }
4204
4618
  async function planRecurseChild(entry, root, client, opts) {
4205
4619
  const dir = resolveChildDir(entry.path, root);
4206
- if (!existsSync10(dir)) {
4620
+ if (!existsSync11(dir)) {
4207
4621
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4208
4622
  }
4209
- if (existsSync10(join12(dir, ".sechroom.json"))) {
4623
+ if (existsSync11(join13(dir, ".sechroom.json"))) {
4210
4624
  return {
4211
4625
  label: entry.path,
4212
4626
  dir,
@@ -4279,7 +4693,7 @@ This fan-out will pin the same lane in every repo:
4279
4693
  async function runRecurse(cfg, g, opts) {
4280
4694
  const { yes, dryRun, json } = opts;
4281
4695
  const root = process.cwd();
4282
- const manifestPath = join12(root, ".sechroom", "repos.json");
4696
+ const manifestPath = join13(root, ".sechroom", "repos.json");
4283
4697
  const fromManifest = readManifest(manifestPath);
4284
4698
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4285
4699
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4791,23 +5205,23 @@ Examples:
4791
5205
 
4792
5206
  // src/commands/reset.ts
4793
5207
  import { homedir as homedir4 } from "os";
4794
- import { join as join13 } from "path";
4795
- import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
5208
+ import { join as join14 } from "path";
5209
+ import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
4796
5210
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4797
- var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
4798
- var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
4799
- var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
4800
- var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
5211
+ var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
5212
+ var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
5213
+ var localAgentsDir = () => join14(process.cwd(), ".claude", "agents");
5214
+ var globalAgentsDir = () => join14(homedir4(), ".claude", "agents");
4801
5215
  function removeMaterialisedSkills(dir) {
4802
5216
  const removed = [];
4803
- const lockPath = join13(dir, SKILLS_LOCK2);
4804
- if (!existsSync11(lockPath)) return removed;
5217
+ const lockPath = join14(dir, SKILLS_LOCK2);
5218
+ if (!existsSync12(lockPath)) return removed;
4805
5219
  try {
4806
- const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
5220
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
4807
5221
  for (const entry of Object.values(lock)) {
4808
5222
  for (const name of entry.skills ?? []) {
4809
- const p = join13(dir, name);
4810
- if (existsSync11(p)) {
5223
+ const p = join14(dir, name);
5224
+ if (existsSync12(p)) {
4811
5225
  rmSync3(p, { recursive: true, force: true });
4812
5226
  removed.push(p);
4813
5227
  }
@@ -4852,18 +5266,18 @@ function registerReset(program2) {
4852
5266
  }
4853
5267
  }
4854
5268
  const removed = [];
4855
- const stateDir = join13(process.cwd(), ".sechroom");
4856
- if (existsSync11(stateDir)) {
5269
+ const stateDir = join14(process.cwd(), ".sechroom");
5270
+ if (existsSync12(stateDir)) {
4857
5271
  rmSync3(stateDir, { recursive: true, force: true });
4858
5272
  removed.push(stateDir);
4859
5273
  }
4860
- const legacyCfg = join13(process.cwd(), ".sechroom.json");
4861
- if (existsSync11(legacyCfg)) {
5274
+ const legacyCfg = join14(process.cwd(), ".sechroom.json");
5275
+ if (existsSync12(legacyCfg)) {
4862
5276
  rmSync3(legacyCfg, { force: true });
4863
5277
  removed.push(legacyCfg);
4864
5278
  }
4865
- const legacySem = join13(process.cwd(), ".sem");
4866
- if (existsSync11(legacySem)) {
5279
+ const legacySem = join14(process.cwd(), ".sem");
5280
+ if (existsSync12(legacySem)) {
4867
5281
  rmSync3(legacySem, { force: true });
4868
5282
  removed.push(legacySem);
4869
5283
  }
@@ -4889,8 +5303,8 @@ function registerReset(program2) {
4889
5303
  }
4890
5304
 
4891
5305
  // src/commands/skills.ts
4892
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
4893
- import { join as join14 } from "path";
5306
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, statSync as statSync4, writeFileSync as writeFileSync11 } from "fs";
5307
+ import { join as join15 } from "path";
4894
5308
  function filenameFromDisposition(header) {
4895
5309
  if (!header) return void 0;
4896
5310
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -4898,11 +5312,11 @@ function filenameFromDisposition(header) {
4898
5312
  }
4899
5313
  function resolveOutputPath(output, serverFilename) {
4900
5314
  const filename = serverFilename || "skills.zip";
4901
- if (!output) return join14(process.cwd(), filename);
4902
- const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
5315
+ if (!output) return join15(process.cwd(), filename);
5316
+ const looksLikeDir = output.endsWith("/") || existsSync13(output) && statSync4(output).isDirectory();
4903
5317
  if (looksLikeDir) {
4904
- mkdirSync10(output, { recursive: true });
4905
- return join14(output, filename);
5318
+ mkdirSync11(output, { recursive: true });
5319
+ return join15(output, filename);
4906
5320
  }
4907
5321
  return output;
4908
5322
  }
@@ -4933,7 +5347,7 @@ async function downloadZip(label, call, output) {
4933
5347
  const buf = Buffer.from(res.data);
4934
5348
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
4935
5349
  const path = resolveOutputPath(output, filename);
4936
- writeFileSync10(path, buf);
5350
+ writeFileSync11(path, buf);
4937
5351
  return { path, bytes: buf.length, filename };
4938
5352
  }
4939
5353
  function registerSkills(program2) {
@@ -5101,12 +5515,12 @@ Examples:
5101
5515
  }
5102
5516
 
5103
5517
  // src/commands/sweep.ts
5104
- import { existsSync as existsSync13 } from "fs";
5105
- import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
5106
- var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
5518
+ import { existsSync as existsSync14 } from "fs";
5519
+ import { dirname as dirname9, join as join16, resolve as resolve2 } from "path";
5520
+ var DEFAULT_MANIFEST = join16(".sechroom", "repos.json");
5107
5521
  function planEntry(entry, root) {
5108
5522
  const dir = resolveChildDir(entry.path, root);
5109
- if (!existsSync13(dir)) {
5523
+ if (!existsSync14(dir)) {
5110
5524
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5111
5525
  }
5112
5526
  if (committedBindingPath(dir)) {
@@ -5182,7 +5596,7 @@ Examples:
5182
5596
  `);
5183
5597
  return;
5184
5598
  }
5185
- const root = dirname8(dirname8(manifestPath));
5599
+ const root = dirname9(dirname9(manifestPath));
5186
5600
  const plans = repos.map((entry) => planEntry(entry, root));
5187
5601
  if (!json) {
5188
5602
  process.stderr.write(
@@ -5201,13 +5615,13 @@ Examples:
5201
5615
 
5202
5616
  // src/commands/telemetry.ts
5203
5617
  import {
5204
- existsSync as existsSync14,
5205
- mkdirSync as mkdirSync11,
5206
- readFileSync as readFileSync9,
5618
+ existsSync as existsSync15,
5619
+ mkdirSync as mkdirSync12,
5620
+ readFileSync as readFileSync11,
5207
5621
  rmSync as rmSync4,
5208
- writeFileSync as writeFileSync11
5622
+ writeFileSync as writeFileSync12
5209
5623
  } from "fs";
5210
- import { dirname as dirname9, join as join16 } from "path";
5624
+ import { dirname as dirname10, join as join17 } from "path";
5211
5625
  function registerTelemetry(program2) {
5212
5626
  const telemetry = program2.command("telemetry").description(
5213
5627
  "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
@@ -5277,14 +5691,14 @@ function registerTelemetry(program2) {
5277
5691
  "Decomposition id this session executes"
5278
5692
  ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5279
5693
  const json = Boolean(cmd.optsWithGlobals().json);
5280
- const dir = join16(process.cwd(), ".sechroom");
5281
- mkdirSync11(dir, { recursive: true });
5282
- const path = join16(dir, BINDING_FILE);
5694
+ const dir = join17(process.cwd(), ".sechroom");
5695
+ mkdirSync12(dir, { recursive: true });
5696
+ const path = join17(dir, BINDING_FILE);
5283
5697
  const binding = {
5284
5698
  decompositionId: opts.decomposition,
5285
5699
  taskId: opts.task
5286
5700
  };
5287
- writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
5701
+ writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
5288
5702
  ensureStateDirIgnored(process.cwd());
5289
5703
  if (json) {
5290
5704
  emit({ bound: true, ...binding, path }, true);
@@ -5299,8 +5713,8 @@ function registerTelemetry(program2) {
5299
5713
  });
5300
5714
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5301
5715
  const json = Boolean(cmd.optsWithGlobals().json);
5302
- const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
5303
- const existed = existsSync14(path);
5716
+ const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
5717
+ const existed = existsSync15(path);
5304
5718
  if (existed) rmSync4(path);
5305
5719
  if (json) emit({ unbound: existed, path }, true);
5306
5720
  else
@@ -5414,11 +5828,11 @@ async function postTelemetry(cfg, decompositionId, events) {
5414
5828
  function findBinding(start) {
5415
5829
  let dir = start;
5416
5830
  for (; ; ) {
5417
- const path = join16(dir, ".sechroom", BINDING_FILE);
5418
- if (existsSync14(path)) {
5831
+ const path = join17(dir, ".sechroom", BINDING_FILE);
5832
+ if (existsSync15(path)) {
5419
5833
  try {
5420
5834
  const b = JSON.parse(
5421
- readFileSync9(path, "utf8")
5835
+ readFileSync11(path, "utf8")
5422
5836
  );
5423
5837
  if (b.decompositionId && b.taskId)
5424
5838
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5426,18 +5840,18 @@ function findBinding(start) {
5426
5840
  }
5427
5841
  return null;
5428
5842
  }
5429
- const parent = dirname9(dir);
5843
+ const parent = dirname10(dir);
5430
5844
  if (parent === dir) return null;
5431
5845
  dir = parent;
5432
5846
  }
5433
5847
  }
5434
5848
  function parseTranscript(path) {
5435
- if (!existsSync14(path)) return null;
5849
+ if (!existsSync15(path)) return null;
5436
5850
  let tokensIn = 0;
5437
5851
  let tokensOut = 0;
5438
5852
  let contextUsed = 0;
5439
5853
  let model = "";
5440
- for (const line of readFileSync9(path, "utf8").split("\n")) {
5854
+ for (const line of readFileSync11(path, "utf8").split("\n")) {
5441
5855
  if (!line.trim()) continue;
5442
5856
  let obj;
5443
5857
  try {
@@ -5454,11 +5868,12 @@ function parseTranscript(path) {
5454
5868
  if (obj.message?.model) model = obj.message.model;
5455
5869
  }
5456
5870
  if (tokensIn === 0 && tokensOut === 0) return null;
5457
- return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
5871
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
5458
5872
  }
5459
- function windowFor(model) {
5873
+ function windowFor(model, contextUsed = 0) {
5460
5874
  const m = model.toLowerCase();
5461
- return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
5875
+ if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
5876
+ return contextUsed > 2e5 ? 1e6 : 2e5;
5462
5877
  }
5463
5878
  async function readStdin2() {
5464
5879
  if (process.stdin.isTTY) return "";
@@ -5694,7 +6109,7 @@ Examples:
5694
6109
  function resolveVersion() {
5695
6110
  try {
5696
6111
  const pkg = JSON.parse(
5697
- readFileSync10(new URL("../package.json", import.meta.url), "utf8")
6112
+ readFileSync12(new URL("../package.json", import.meta.url), "utf8")
5698
6113
  );
5699
6114
  return pkg.version ?? "0.0.0";
5700
6115
  } catch {
@@ -5852,6 +6267,8 @@ registerLookup(program);
5852
6267
  registerRelationships(program);
5853
6268
  registerWorkspace(program);
5854
6269
  registerProject(program);
6270
+ registerDecomposition(program);
6271
+ registerClose(program);
5855
6272
  registerFiling(program);
5856
6273
  registerContinuity(program);
5857
6274
  registerCheckpoint(program);