@sechroom/cli 2026.6.239-rc.6418da97 → 2026.7.1-rc.2c16fefa

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 +924 -542
  2. package/package.json +4 -3
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
@@ -433,6 +433,7 @@ async function requireToken(cfg) {
433
433
  import createClient from "openapi-fetch";
434
434
 
435
435
  // src/ui.ts
436
+ import * as clack from "@clack/prompts";
436
437
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
437
438
  var quiet = false;
438
439
  function setQuiet(q) {
@@ -458,7 +459,7 @@ var err = (s) => style.red(s);
458
459
  function active() {
459
460
  return !quiet && Boolean(process.stderr.isTTY);
460
461
  }
461
- function spinner(text) {
462
+ function spinner(text2) {
462
463
  if (!active()) {
463
464
  return { succeed() {
464
465
  }, fail() {
@@ -468,9 +469,9 @@ function spinner(text) {
468
469
  let i = 0;
469
470
  const render = () => {
470
471
  i = (i + 1) % FRAMES.length;
471
- process.stderr.write(`\r${FRAMES[i]} ${text}`);
472
+ process.stderr.write(`\r${FRAMES[i]} ${text2}`);
472
473
  };
473
- process.stderr.write(`\r${FRAMES[0]} ${text}`);
474
+ process.stderr.write(`\r${FRAMES[0]} ${text2}`);
474
475
  const timer = setInterval(render, 80);
475
476
  timer.unref?.();
476
477
  const clear = () => {
@@ -480,12 +481,12 @@ function spinner(text) {
480
481
  return {
481
482
  succeed(t) {
482
483
  clear();
483
- process.stderr.write(`${ok("\u2713")} ${t ?? text}
484
+ process.stderr.write(`${ok("\u2713")} ${t ?? text2}
484
485
  `);
485
486
  },
486
487
  fail(t) {
487
488
  clear();
488
- process.stderr.write(`${err("\u2717")} ${t ?? text}
489
+ process.stderr.write(`${err("\u2717")} ${t ?? text2}
489
490
  `);
490
491
  },
491
492
  stop: clear
@@ -496,31 +497,24 @@ function canPrompt() {
496
497
  }
497
498
  async function promptYesNo(question) {
498
499
  if (!canPrompt()) return false;
499
- const { createInterface } = await import("readline");
500
- const rl = createInterface({ input: process.stdin, output: process.stderr });
501
- try {
502
- const answer = await new Promise((resolve3) => {
503
- rl.question(`${question} [y/N] `, resolve3);
504
- });
505
- return /^y(es)?$/i.test(answer.trim());
506
- } finally {
507
- rl.close();
508
- }
500
+ const r = await clack.confirm({
501
+ message: question,
502
+ initialValue: false,
503
+ output: process.stderr
504
+ });
505
+ return clack.isCancel(r) ? false : r;
509
506
  }
510
507
  async function promptText(question, def) {
511
508
  if (!canPrompt()) return def ?? "";
512
- const { createInterface } = await import("readline");
513
- const rl = createInterface({ input: process.stdin, output: process.stderr });
514
- try {
515
- const suffix = def ? ` [${def}]` : "";
516
- const answer = await new Promise((resolve3) => {
517
- rl.question(`${question}${suffix} `, resolve3);
518
- });
519
- const trimmed = answer.trim();
520
- return trimmed.length > 0 ? trimmed : def ?? "";
521
- } finally {
522
- rl.close();
523
- }
509
+ const r = await clack.text({
510
+ message: question,
511
+ defaultValue: def,
512
+ placeholder: def,
513
+ output: process.stderr
514
+ });
515
+ if (clack.isCancel(r)) return def ?? "";
516
+ const trimmed = (r ?? "").trim();
517
+ return trimmed.length > 0 ? trimmed : def ?? "";
524
518
  }
525
519
  async function promptSelect(question, choices, def) {
526
520
  if (choices.length === 0) throw new Error("promptSelect: no choices");
@@ -529,74 +523,50 @@ async function promptSelect(question, choices, def) {
529
523
  def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
530
524
  );
531
525
  if (!canPrompt()) return choices[defIdx].value;
532
- const { createInterface } = await import("readline");
533
- const rl = createInterface({ input: process.stdin, output: process.stderr });
534
- try {
535
- process.stderr.write(`${style.bold(question)}
536
- `);
537
- choices.forEach((c, i) => {
538
- const marker = i === defIdx ? style.cyan("\u203A") : " ";
539
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
540
- process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
541
- `);
542
- });
543
- const answer = await new Promise((resolve3) => {
544
- rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
545
- });
546
- const trimmed = answer.trim();
547
- if (!trimmed) return choices[defIdx].value;
548
- const n = Number(trimmed);
549
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
550
- const byLabel = choices.find(
551
- (c) => c.label.toLowerCase().startsWith(trimmed.toLowerCase())
552
- );
553
- return byLabel ? byLabel.value : choices[defIdx].value;
554
- } finally {
555
- rl.close();
556
- }
526
+ const r = await clack.select({
527
+ message: question,
528
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
529
+ // can't verify against a generic T; the runtime shape is correct.
530
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
531
+ initialValue: choices[defIdx].value,
532
+ output: process.stderr
533
+ });
534
+ return clack.isCancel(r) ? choices[defIdx].value : r;
557
535
  }
558
536
  async function promptMultiSelect(question, choices, preselected = []) {
559
537
  if (choices.length === 0) return [];
560
- const pre = (v) => preselected.includes(v);
561
- const preValues = () => choices.filter((c) => pre(c.value)).map((c) => c.value);
562
- if (!canPrompt()) return preValues();
563
- const { createInterface } = await import("readline");
564
- const rl = createInterface({ input: process.stdin, output: process.stderr });
565
- try {
566
- process.stderr.write(
567
- `${style.bold(question)} ${style.dim("(numbers or 'all', comma-separated; Enter keeps \u25C9)")}
568
- `
569
- );
570
- choices.forEach((c, i) => {
571
- const box = pre(c.value) ? style.cyan("\u25C9") : "\u25CB";
572
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
573
- process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
574
- `);
575
- });
576
- const answer = await new Promise((resolve3) => {
577
- rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
578
- });
579
- const trimmed = answer.trim().toLowerCase();
580
- if (!trimmed) return preValues();
581
- if (trimmed === "all") return choices.map((c) => c.value);
582
- const picks = [];
583
- for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
584
- const n = Number(tok);
585
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
586
- picks.push(choices[n - 1].value);
587
- continue;
588
- }
589
- const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
590
- if (byLabel) picks.push(byLabel.value);
591
- }
592
- const uniq = [...new Set(picks)];
593
- return uniq.length > 0 ? uniq : preValues();
594
- } finally {
595
- rl.close();
596
- }
597
- }
598
- async function withSpinner(text, fn) {
599
- const s = spinner(text);
538
+ const preValues = choices.filter((c) => preselected.includes(c.value)).map((c) => c.value);
539
+ if (!canPrompt()) return preValues;
540
+ const r = await clack.multiselect({
541
+ message: question,
542
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
543
+ // can't verify against a generic T; the runtime shape is correct.
544
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
545
+ initialValues: preValues,
546
+ required: false,
547
+ output: process.stderr
548
+ });
549
+ return clack.isCancel(r) ? preValues : r;
550
+ }
551
+ async function promptAutocomplete(question, choices, def) {
552
+ if (choices.length === 0) throw new Error("promptAutocomplete: no choices");
553
+ const defIdx = Math.max(
554
+ 0,
555
+ def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
556
+ );
557
+ if (!canPrompt()) return choices[defIdx].value;
558
+ const r = await clack.autocomplete({
559
+ message: question,
560
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
561
+ // can't verify against a generic T; the runtime shape is correct.
562
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
563
+ placeholder: "Type to filter\u2026",
564
+ output: process.stderr
565
+ });
566
+ return clack.isCancel(r) ? choices[defIdx].value : r;
567
+ }
568
+ async function withSpinner(text2, fn) {
569
+ const s = spinner(text2);
600
570
  try {
601
571
  const result = await fn();
602
572
  s.succeed();
@@ -1330,12 +1300,228 @@ workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
1330
1300
  }
1331
1301
 
1332
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";
1333
1305
  import {
1334
1306
  HttpTransportType,
1335
1307
  HubConnectionBuilder
1336
1308
  } from "@microsoft/signalr";
1337
1309
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1338
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
1339
1525
  function registerChannel(program2) {
1340
1526
  const channel = program2.command("channel").description(
1341
1527
  "Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
@@ -1358,11 +1544,16 @@ function registerChannel(program2) {
1358
1544
  const cfg = resolveConfig(cmd.optsWithGlobals());
1359
1545
  const filter = readFilter(opts);
1360
1546
  const sub = await ensureSubscription(cfg, opts.name, filter);
1361
- const conn = await openConnection(cfg, (payload) => {
1362
- process.stdout.write(
1547
+ const seen = /* @__PURE__ */ new Set();
1548
+ const deliver = makeDeliver(
1549
+ filter,
1550
+ seen,
1551
+ (payload) => process.stdout.write(
1363
1552
  (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
1364
- );
1365
- });
1553
+ )
1554
+ );
1555
+ const conn = await openConnection(cfg, deliver);
1556
+ await reconcile(cfg, filter, deliver);
1366
1557
  if (json) {
1367
1558
  emit(
1368
1559
  {
@@ -1399,7 +1590,8 @@ function registerChannel(program2) {
1399
1590
  );
1400
1591
  await mcp.connect(new StdioServerTransport());
1401
1592
  await ensureSubscription(cfg, opts.name, filter);
1402
- const conn = await openConnection(cfg, (payload) => {
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ const deliver = makeDeliver(filter, seen, (payload) => {
1403
1595
  const { content, meta } = summarizeEvent(payload);
1404
1596
  void mcp.notification({
1405
1597
  method: "notifications/claude/channel",
@@ -1409,6 +1601,8 @@ function registerChannel(program2) {
1409
1601
  `))
1410
1602
  );
1411
1603
  });
1604
+ const conn = await openConnection(cfg, deliver);
1605
+ await reconcile(cfg, filter, deliver);
1412
1606
  process.stderr.write(
1413
1607
  style.dim(
1414
1608
  `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
@@ -1417,6 +1611,55 @@ function registerChannel(program2) {
1417
1611
  );
1418
1612
  await holdOpen(conn);
1419
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
+ });
1420
1663
  channel.addHelpText(
1421
1664
  "after",
1422
1665
  `
@@ -1425,11 +1668,23 @@ Examples:
1425
1668
  $ sechroom channel connect --tag kind:task --tag status:in-progress
1426
1669
  $ sechroom channel connect --workspace wsp_X --json | jq .
1427
1670
 
1428
- # As a Claude Code channel (research preview, v2.1.80+):
1429
- # add to .mcp.json: { "mcpServers": { "sechroom": { "command": "npx", "args": ["@sechroom/cli", "channel", "mcp"] } } }
1430
- # 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`
1431
1674
  );
1432
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
+ }
1433
1688
  function readFilter(opts) {
1434
1689
  const tags = opts.tag ?? [];
1435
1690
  const workspaceScope = opts.workspace ?? [];
@@ -1482,20 +1737,134 @@ function holdOpen(conn) {
1482
1737
  process.on("SIGTERM", stop);
1483
1738
  });
1484
1739
  }
1485
- function summarizeEvent(payload) {
1740
+ function parseEvent(payload) {
1486
1741
  let data = payload;
1487
1742
  if (typeof payload === "string") {
1488
1743
  try {
1489
1744
  data = JSON.parse(payload);
1490
1745
  } catch {
1491
- return { content: payload, meta: {} };
1746
+ return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
1492
1747
  }
1493
1748
  }
1494
1749
  const obj = data ?? {};
1495
1750
  const inner = obj.data ?? obj;
1496
- const eventType = str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event";
1497
- const memoryId = str(inner.memoryId ?? inner.MemoryId);
1498
- 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);
1499
1868
  const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
1500
1869
  const meta = {};
1501
1870
  if (eventType) meta.event_type = eventType;
@@ -1521,7 +1890,7 @@ Examples:
1521
1890
  $ sechroom chat replies 1718049600.123456 --surface slack
1522
1891
  $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
1523
1892
  );
1524
- chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text, opts, cmd) => {
1893
+ chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
1525
1894
  const { surface, ...globals } = cmd.optsWithGlobals();
1526
1895
  const json = Boolean(cmd.optsWithGlobals().json);
1527
1896
  const cfg = resolveConfig(globals);
@@ -1531,7 +1900,7 @@ Examples:
1531
1900
  params: { path: { surface: String(surface) } },
1532
1901
  body: {
1533
1902
  channelId,
1534
- text,
1903
+ text: text2,
1535
1904
  guildId: opts.guild ?? null,
1536
1905
  attachedMemoryId: opts.memory ?? null,
1537
1906
  trackReplies: opts.track,
@@ -1590,28 +1959,28 @@ Examples:
1590
1959
  }
1591
1960
 
1592
1961
  // src/commands/checkpoint.ts
1593
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1594
- 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";
1595
1964
 
1596
1965
  // src/commands/hook.ts
1597
1966
  import { createHash as createHash2 } from "crypto";
1598
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
1599
- 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";
1600
1969
 
1601
1970
  // src/sem.ts
1602
- import { dirname as dirname2, join as join5 } from "path";
1603
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1604
- 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");
1605
1974
  var STATE_DIR_NAME2 = ".sechroom";
1606
1975
  function localSemPath(cwd = process.cwd()) {
1607
- return join5(cwd, SEM_FILE);
1976
+ return join8(cwd, SEM_FILE);
1608
1977
  }
1609
1978
  function resolveSemPathForRead(start = process.cwd()) {
1610
1979
  let dir = start;
1611
1980
  while (true) {
1612
- const candidate = join5(dir, SEM_FILE);
1613
- if (existsSync4(candidate)) return candidate;
1614
- const parent = dirname2(dir);
1981
+ const candidate = join8(dir, SEM_FILE);
1982
+ if (existsSync7(candidate)) return candidate;
1983
+ const parent = dirname5(dir);
1615
1984
  if (parent === dir) return void 0;
1616
1985
  dir = parent;
1617
1986
  }
@@ -1621,23 +1990,23 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1621
1990
  let dir = start;
1622
1991
  let gitPath;
1623
1992
  for (; ; ) {
1624
- const candidate = join5(dir, ".git");
1625
- if (existsSync4(candidate)) {
1993
+ const candidate = join8(dir, ".git");
1994
+ if (existsSync7(candidate)) {
1626
1995
  gitPath = candidate;
1627
1996
  break;
1628
1997
  }
1629
- const parent = dirname2(dir);
1998
+ const parent = dirname5(dir);
1630
1999
  if (parent === dir) break;
1631
2000
  dir = parent;
1632
2001
  }
1633
2002
  if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1634
- const gitFile = readFileSync3(gitPath, "utf8");
2003
+ const gitFile = readFileSync5(gitPath, "utf8");
1635
2004
  const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1636
2005
  if (!common) return lane;
1637
- const worktreesDir = join5(common[1], "worktrees");
2006
+ const worktreesDir = join8(common[1], "worktrees");
1638
2007
  const siblings = readdirSync(worktreesDir).filter((n) => {
1639
2008
  try {
1640
- return statSync(join5(worktreesDir, n)).isDirectory();
2009
+ return statSync(join8(worktreesDir, n)).isDirectory();
1641
2010
  } catch {
1642
2011
  return false;
1643
2012
  }
@@ -1650,326 +2019,114 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1650
2019
  function laneWithWorktreeSuffix(lane, gitFile, siblings) {
1651
2020
  const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
1652
2021
  if (!m) return lane;
1653
- const idx = [...siblings].sort().indexOf(m[1]);
1654
- return idx < 0 ? lane : `${lane}-${idx + 2}`;
1655
- }
1656
- function serializeSem(values) {
1657
- return JSON.stringify(values, null, 2) + "\n";
1658
- }
1659
- function readSem(path) {
1660
- const p = path ?? resolveSemPathForRead();
1661
- if (!p || !existsSync4(p)) return void 0;
1662
- return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
1663
- }
1664
- function readLocalSemValues(cwd = process.cwd()) {
1665
- const next = join5(cwd, SEM_FILE);
1666
- if (existsSync4(next)) return readSem(next)?.values ?? {};
1667
- return {};
1668
- }
1669
- function parseLaneJson(text) {
1670
- try {
1671
- const parsed = JSON.parse(text);
1672
- const out = {};
1673
- for (const [k, v] of Object.entries(parsed)) {
1674
- if (typeof v === "string") out[k] = v;
1675
- }
1676
- return out;
1677
- } catch {
1678
- return {};
1679
- }
1680
- }
1681
- var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1682
- function writeSem(values, path = localSemPath()) {
1683
- mkdirSync4(dirname2(path), { recursive: true });
1684
- writeFileSync4(path, serializeSem(values));
1685
- ensureSemIgnored(path);
1686
- ensureContinuityScaffold(path);
1687
- return path;
1688
- }
1689
- function ensureStateDirIgnored(cwd = process.cwd()) {
1690
- ensureSemIgnored(localSemPath(cwd));
1691
- }
1692
- var CONTINUITY_FILE_NAME = "continuity.json";
1693
- var CONTINUITY_SCAFFOLD = JSON.stringify(
1694
- {
1695
- _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.",
1696
- objective: "",
1697
- state: "",
1698
- lastAction: "",
1699
- nextAction: "",
1700
- resumeInstruction: "",
1701
- constraints: [],
1702
- questions: [],
1703
- artifacts: [],
1704
- confidence: null
1705
- },
1706
- null,
1707
- 2
1708
- ) + "\n";
1709
- function ensureContinuityScaffold(semPath) {
1710
- try {
1711
- const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
1712
- if (existsSync4(target)) return;
1713
- writeFileSync4(target, CONTINUITY_SCAFFOLD);
1714
- } catch {
1715
- }
1716
- }
1717
- function ignoresSem(content) {
1718
- return content.split("\n").some((line) => {
1719
- const t = line.trim();
1720
- 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}`;
1721
- });
1722
- }
1723
- function inGitRepo(startDir) {
1724
- let dir = startDir;
1725
- for (; ; ) {
1726
- if (existsSync4(join5(dir, ".git"))) return true;
1727
- const parent = dirname2(dir);
1728
- if (parent === dir) return false;
1729
- dir = parent;
1730
- }
1731
- }
1732
- function resolveGitignoreTarget(startDir) {
1733
- let dir = startDir;
1734
- for (; ; ) {
1735
- const gi = join5(dir, ".gitignore");
1736
- if (existsSync4(gi)) return { path: gi, exists: true };
1737
- const parent = dirname2(dir);
1738
- if (existsSync4(join5(dir, ".git")) || parent === dir) {
1739
- return { path: join5(startDir, ".gitignore"), exists: false };
1740
- }
1741
- dir = parent;
1742
- }
1743
- }
1744
- function ensureSemIgnored(semPath) {
1745
- try {
1746
- const checkoutDir = dirname2(dirname2(semPath));
1747
- if (!inGitRepo(checkoutDir)) return;
1748
- const target = resolveGitignoreTarget(checkoutDir);
1749
- if (target.exists) {
1750
- const content = readFileSync3(target.path, "utf8");
1751
- if (ignoresSem(content)) return;
1752
- const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1753
- appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1754
- `);
1755
- } else {
1756
- writeFileSync4(target.path, `${STATE_DIR_IGNORE}
1757
- `);
1758
- }
1759
- } catch {
1760
- }
1761
- }
1762
-
1763
- // src/commands/hook-install.ts
1764
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1765
- import { delimiter, dirname as dirname4, join as join7 } from "path";
1766
-
1767
- // src/setup/clients.ts
1768
- import { existsSync as existsSync5 } from "fs";
1769
- import { homedir as homedir3 } from "os";
1770
- import { dirname as dirname3, join as join6 } from "path";
1771
- function claudeDesktopConfigPath(home) {
1772
- switch (process.platform) {
1773
- case "darwin":
1774
- return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1775
- case "win32":
1776
- return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1777
- default:
1778
- return join6(home, ".config", "Claude", "claude_desktop_config.json");
1779
- }
1780
- }
1781
- function clientTargets(cwd, opts = {}) {
1782
- const home = homedir3();
1783
- const claudeDir = opts.claudeDir ?? join6(home, ".claude");
1784
- const codexHome = opts.codexHome ?? join6(home, ".codex");
1785
- return {
1786
- "claude-code": {
1787
- key: "claude-code",
1788
- label: "Claude Code",
1789
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
1790
- instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
1791
- },
1792
- "claude-desktop": {
1793
- key: "claude-desktop",
1794
- label: "Claude Desktop",
1795
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1796
- instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
1797
- },
1798
- codex: {
1799
- key: "codex",
1800
- label: "Codex CLI",
1801
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
1802
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1803
- },
1804
- cursor: {
1805
- key: "cursor",
1806
- label: "Cursor",
1807
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
1808
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1809
- },
1810
- antigravity: {
1811
- key: "antigravity",
1812
- label: "Google Antigravity",
1813
- // FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
1814
- // `~/.gemini/config/mcp_config.json` (not cwd; not affected by
1815
- // CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
1816
- // `type` — comes from the `antigravity` server surface, so we don't
1817
- // hardcode it here. Instructions go in the project `AGENTS.md`
1818
- // (cross-tool, shared with Codex/Cursor).
1819
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1820
- instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
1821
- }
1822
- };
1823
- }
1824
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
1825
- var DEFAULT_CLIENT_KEY = "claude-code";
1826
- function detectInstalledClients(cwd) {
1827
- const home = homedir3();
1828
- const detected = [];
1829
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
1830
- if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1831
- if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
1832
- if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
1833
- if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
1834
- return detected;
1835
- }
1836
-
1837
- // src/commands/hook-install.ts
1838
- var CLAUDE_HOOK_COMMANDS = {
1839
- SessionStart: "sechroom hook session-start",
1840
- PreCompact: "sechroom hook pre-compact",
1841
- SessionEnd: "sechroom hook session-end",
1842
- // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1843
- // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1844
- // for every Claude install. Claude-only (it parses a Claude Code transcript).
1845
- Stop: "sechroom telemetry hook"
1846
- };
1847
- var CODEX_HOOK_COMMANDS = {
1848
- SessionStart: "sechroom hook session-start",
1849
- Stop: "sechroom hook session-end --debounce-minutes 10"
1850
- };
1851
- function hasHookCommand(config2, event, command) {
1852
- const groups = config2.hooks?.[event] ?? [];
1853
- return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1854
- }
1855
- function mergeHooks(config2, commands) {
1856
- config2.hooks ??= {};
1857
- let added = 0;
1858
- for (const [event, command] of Object.entries(commands)) {
1859
- if (hasHookCommand(config2, event, command)) continue;
1860
- const groups = config2.hooks[event] ??= [];
1861
- groups.push({ hooks: [{ type: "command", command }] });
1862
- added += 1;
1863
- }
1864
- return added;
2022
+ const idx = [...siblings].sort().indexOf(m[1]);
2023
+ return idx < 0 ? lane : `${lane}-${idx + 2}`;
1865
2024
  }
1866
- function readJsonConfig2(path) {
1867
- if (!existsSync6(path)) return {};
1868
- const raw = readFileSync4(path, "utf8");
1869
- if (!raw.trim()) return {};
1870
- return JSON.parse(raw);
2025
+ function serializeSem(values) {
2026
+ return JSON.stringify(values, null, 2) + "\n";
1871
2027
  }
1872
- function installHooksJson(path, commands, dryRun) {
1873
- const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
1874
- const config2 = readJsonConfig2(path);
1875
- const added = mergeHooks(config2, commands);
1876
- if (added === 0 && existed) return { path, status: "current" };
1877
- if (!dryRun) {
1878
- mkdirSync5(dirname4(path), { recursive: true });
1879
- writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1880
- }
1881
- 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")) };
1882
2032
  }
1883
- function installClaudeCommands(claudeDir, commands, dryRun) {
1884
- 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 {};
1885
2037
  }
1886
- function ensureCodexFeaturesHooks(content) {
1887
- const lines = content.split("\n");
1888
- const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1889
- if (headerIdx === -1) {
1890
- const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1891
- return { next: base + "\n[features]\nhooks = true\n", changed: true };
1892
- }
1893
- for (let i = headerIdx + 1; i < lines.length; i += 1) {
1894
- const trimmed = lines[i].trim();
1895
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1896
- const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1897
- if (!m) continue;
1898
- const value = m[4].replace(/\s*#.*$/, "").trim();
1899
- if (value === "true") return { next: content, changed: false };
1900
- lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1901
- 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 {};
1902
2048
  }
1903
- lines.splice(headerIdx + 1, 0, "hooks = true");
1904
- return { next: lines.join("\n"), changed: true };
1905
2049
  }
1906
- function installCodexFeatureFlag(path, dryRun) {
1907
- const existed = existsSync6(path);
1908
- const content = existed ? readFileSync4(path, "utf8") : "";
1909
- const { next, changed } = ensureCodexFeaturesHooks(content);
1910
- if (!changed) return { path, status: "current" };
1911
- if (!dryRun) {
1912
- mkdirSync5(dirname4(path), { recursive: true });
1913
- 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 {
1914
2084
  }
1915
- return { path, status: existed ? "merged" : "created" };
1916
2085
  }
1917
- function resolveSurfaces(surface, cwd) {
1918
- if (surface === "claude") return ["claude"];
1919
- if (surface === "codex") return ["codex"];
1920
- if (surface === "both") return ["claude", "codex"];
1921
- if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1922
- const surfaces = detectHookSurfaces(cwd);
1923
- 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
+ });
1924
2091
  }
1925
- function describe(result, dryRun) {
1926
- if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1927
- const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1928
- 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
+ }
1929
2100
  }
1930
- var HOOK_SURFACE_LABEL = {
1931
- claude: "Claude Code",
1932
- codex: "Codex"
1933
- };
1934
- function installHookSurfaces(surfaces, opts) {
1935
- const out = [];
1936
- for (const surface of surfaces) {
1937
- if (surface === "claude") {
1938
- const path = join7(opts.claudeDir, "settings.json");
1939
- out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1940
- } else {
1941
- const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1942
- const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
1943
- 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 };
1944
2109
  }
2110
+ dir = parent;
1945
2111
  }
1946
- return out;
1947
- }
1948
- function detectHookSurfaces(cwd) {
1949
- const detected = detectInstalledClients(cwd);
1950
- const surfaces = [];
1951
- if (detected.includes("claude-code")) surfaces.push("claude");
1952
- if (detected.includes("codex")) surfaces.push("codex");
1953
- return surfaces;
1954
2112
  }
1955
- function isSechroomOnPath() {
1956
- const pathEnv = process.env.PATH ?? "";
1957
- if (!pathEnv) return false;
1958
- const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1959
- for (const dir of pathEnv.split(delimiter)) {
1960
- if (!dir) continue;
1961
- for (const name of names) {
1962
- 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
+ `);
1963
2127
  }
2128
+ } catch {
1964
2129
  }
1965
- return false;
1966
- }
1967
- function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1968
- if (isSechroomOnPath()) return false;
1969
- write(
1970
- "\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"
1971
- );
1972
- return true;
1973
2130
  }
1974
2131
 
1975
2132
  // src/commands/hook.ts
@@ -1996,13 +2153,13 @@ function resolveLane(flagLane, cwd) {
1996
2153
  if (!base) return void 0;
1997
2154
  return applyWorktreeLaneSuffix(base, start);
1998
2155
  }
1999
- var INTENT_FILE = join8(".sechroom", "continuity.json");
2156
+ var INTENT_FILE = join9(".sechroom", "continuity.json");
2000
2157
  function resolveIntentPath(start) {
2001
2158
  let dir = start;
2002
2159
  for (; ; ) {
2003
- const candidate = join8(dir, INTENT_FILE);
2004
- if (existsSync7(candidate)) return candidate;
2005
- const parent = dirname5(dir);
2160
+ const candidate = join9(dir, INTENT_FILE);
2161
+ if (existsSync8(candidate)) return candidate;
2162
+ const parent = dirname6(dir);
2006
2163
  if (parent === dir) return void 0;
2007
2164
  dir = parent;
2008
2165
  }
@@ -2011,7 +2168,7 @@ function readIntent(start) {
2011
2168
  const path = resolveIntentPath(start);
2012
2169
  if (!path) return void 0;
2013
2170
  try {
2014
- return JSON.parse(readFileSync5(path, "utf8"));
2171
+ return JSON.parse(readFileSync6(path, "utf8"));
2015
2172
  } catch {
2016
2173
  return void 0;
2017
2174
  }
@@ -2053,14 +2210,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
2053
2210
  }
2054
2211
  function ledgerPath(start) {
2055
2212
  const intent = resolveIntentPath(start);
2056
- const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
2057
- return join8(dir, ".checkpoint-state.json");
2213
+ const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
2214
+ return join9(dir, ".checkpoint-state.json");
2058
2215
  }
2059
2216
  function readLedger(start) {
2060
2217
  try {
2061
2218
  const p = ledgerPath(start);
2062
- if (!existsSync7(p)) return {};
2063
- return JSON.parse(readFileSync5(p, "utf8"));
2219
+ if (!existsSync8(p)) return {};
2220
+ return JSON.parse(readFileSync6(p, "utf8"));
2064
2221
  } catch {
2065
2222
  return {};
2066
2223
  }
@@ -2107,13 +2264,13 @@ function recordPush(start, intent) {
2107
2264
  } catch {
2108
2265
  mtimeMs = void 0;
2109
2266
  }
2110
- mkdirSync6(dirname5(p), { recursive: true });
2267
+ mkdirSync7(dirname6(p), { recursive: true });
2111
2268
  const ledger = {
2112
2269
  lastEpochMs: Date.now(),
2113
2270
  lastMtimeMs: mtimeMs,
2114
2271
  lastHash: intentHash(intent)
2115
2272
  };
2116
- writeFileSync6(p, JSON.stringify(ledger) + "\n");
2273
+ writeFileSync7(p, JSON.stringify(ledger) + "\n");
2117
2274
  } catch {
2118
2275
  }
2119
2276
  }
@@ -2360,10 +2517,10 @@ Examples:
2360
2517
  const client = await makeClient(cfg);
2361
2518
  return client.POST("/continuity/snapshots", { body });
2362
2519
  });
2363
- const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
2520
+ const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
2364
2521
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2365
- mkdirSync7(dirname6(path), { recursive: true });
2366
- writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
2522
+ mkdirSync8(dirname7(path), { recursive: true });
2523
+ writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
2367
2524
  recordPush(cwd, merged);
2368
2525
  if (json) {
2369
2526
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -2376,6 +2533,149 @@ Examples:
2376
2533
  });
2377
2534
  }
2378
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 source status flip (for tasks whose status is managed elsewhere)"
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) {
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
+
2379
2679
  // src/commands/continuity.ts
2380
2680
  function registerContinuity(program2) {
2381
2681
  const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
@@ -2536,6 +2836,90 @@ Examples:
2536
2836
  });
2537
2837
  }
2538
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 governed brief into a candidate Task graph (POST /governed-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("/governed-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
+
2539
2923
  // src/commands/filing.ts
2540
2924
  function registerFiling(program2) {
2541
2925
  const filing = program2.command("filing").description("Review and act on filing suggestions");
@@ -3047,8 +3431,8 @@ Examples:
3047
3431
 
3048
3432
  // src/setup/apply.ts
3049
3433
  import { createHash as createHash3 } from "crypto";
3050
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
3051
- 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";
3052
3436
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3053
3437
  var MARKER_END = "<!-- @sechroom/cli:end";
3054
3438
  function normalizeBody(s) {
@@ -3101,22 +3485,22 @@ function parseManagedBlock(content, block) {
3101
3485
  return null;
3102
3486
  }
3103
3487
  function ensureDir2(path) {
3104
- mkdirSync8(dirname7(path), { recursive: true });
3488
+ mkdirSync9(dirname8(path), { recursive: true });
3105
3489
  }
3106
3490
  function readOr(path, fallback) {
3107
3491
  try {
3108
- return readFileSync6(path, "utf8");
3492
+ return readFileSync8(path, "utf8");
3109
3493
  } catch {
3110
3494
  return fallback;
3111
3495
  }
3112
3496
  }
3113
3497
  function mergeMcpJson(path, snippet, dryRun) {
3114
3498
  const incoming = JSON.parse(snippet);
3115
- const existed = existsSync8(path);
3499
+ const existed = existsSync9(path);
3116
3500
  let current = {};
3117
3501
  if (existed) {
3118
3502
  try {
3119
- current = JSON.parse(readFileSync6(path, "utf8"));
3503
+ current = JSON.parse(readFileSync8(path, "utf8"));
3120
3504
  } catch {
3121
3505
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3122
3506
  }
@@ -3124,26 +3508,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3124
3508
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3125
3509
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3126
3510
  ensureDir2(path);
3127
- writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3511
+ writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3128
3512
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3129
3513
  }
3130
3514
  function mergeCodexToml(path, snippet, dryRun) {
3131
- const existed = existsSync8(path);
3515
+ const existed = existsSync9(path);
3132
3516
  let body = readOr(path, "");
3133
3517
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3134
3518
  const trimmed = body.trim();
3135
3519
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3136
3520
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3137
3521
  ensureDir2(path);
3138
- writeFileSync8(path, next, { mode: 384 });
3522
+ writeFileSync9(path, next, { mode: 384 });
3139
3523
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3140
3524
  }
3141
3525
  function writeInstructionBlock(path, write, dryRun) {
3142
- const existed = existsSync8(path);
3526
+ const existed = existsSync9(path);
3143
3527
  const next = computeBlockFile(readOr(path, ""), write);
3144
3528
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3145
3529
  ensureDir2(path);
3146
- writeFileSync8(path, next);
3530
+ writeFileSync9(path, next);
3147
3531
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3148
3532
  }
3149
3533
  function computeBlockFile(current, write) {
@@ -3184,7 +3568,7 @@ function applyBlock(path, write, mode, dryRun) {
3184
3568
  const next = computeBlockFile(current, write);
3185
3569
  if (!dryRun) {
3186
3570
  ensureDir2(proposedPath);
3187
- writeFileSync8(proposedPath, next);
3571
+ writeFileSync9(proposedPath, next);
3188
3572
  }
3189
3573
  return {
3190
3574
  kind: "instruction",
@@ -3224,7 +3608,10 @@ async function applyClient(cfg, setup, target, opts) {
3224
3608
  if (!section) {
3225
3609
  actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
3226
3610
  } else {
3227
- const resolved = await resolveInstruction(cfg, section, opts.personalWorkspaceId);
3611
+ const resolved = await withSpinner(
3612
+ `Resolving ${target.label} agent instructions`,
3613
+ () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
3614
+ );
3228
3615
  if (!resolved) {
3229
3616
  actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
3230
3617
  } else {
@@ -3239,7 +3626,10 @@ async function applyClient(cfg, setup, target, opts) {
3239
3626
  }
3240
3627
  const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
3241
3628
  if (conventionsSection) {
3242
- const conventions = await resolveWorkspaceConventions(cfg, conventionsSection);
3629
+ const conventions = await withSpinner(
3630
+ `Composing ${target.label} workspace conventions`,
3631
+ () => resolveWorkspaceConventions(cfg, conventionsSection)
3632
+ );
3243
3633
  if (conventions) {
3244
3634
  const action = applyBlock(
3245
3635
  target.instruction.path,
@@ -3308,8 +3698,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3308
3698
  }
3309
3699
 
3310
3700
  // src/setup/skills-offer.ts
3311
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
3312
- import { join as join10 } from "path";
3701
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
3702
+ import { join as join11 } from "path";
3313
3703
 
3314
3704
  // src/setup/lane-pin.ts
3315
3705
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3425,8 +3815,8 @@ Found ${summary} available to you for ${surface}.
3425
3815
  if (skills.length > 0) {
3426
3816
  const written = [];
3427
3817
  for (const s of skills) {
3428
- mkdirSync9(join10(sDir, s.name), { recursive: true });
3429
- 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");
3430
3820
  written.push(s.name);
3431
3821
  }
3432
3822
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3434,11 +3824,11 @@ Found ${summary} available to you for ${surface}.
3434
3824
  `);
3435
3825
  }
3436
3826
  if (agents.length > 0) {
3437
- mkdirSync9(aDir, { recursive: true });
3827
+ mkdirSync10(aDir, { recursive: true });
3438
3828
  const written = [];
3439
3829
  for (const a of agents) {
3440
3830
  const file = `${a.name}.md`;
3441
- 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");
3442
3832
  written.push(file);
3443
3833
  }
3444
3834
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3667,20 +4057,20 @@ Examples:
3667
4057
  fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
3668
4058
  const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
3669
4059
  const body = typeof opts.body === "string" && opts.body.trim().length > 0 ? opts.body.trim() : "_TODO: write this section, then edit the memo and re-run the regen._";
3670
- const text = `# ${title}
4060
+ const text2 = `# ${title}
3671
4061
 
3672
4062
  ${body}
3673
4063
  `;
3674
4064
  const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
3675
4065
  if (opts.dryRun) {
3676
- emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
4066
+ emit({ dryRun: true, workspaceId, title, kind, tags, text: text2 }, json);
3677
4067
  return;
3678
4068
  }
3679
4069
  const data = await runApi("Authoring convention memo", async () => {
3680
4070
  const client = await makeClient(cfg);
3681
4071
  return client.POST("/memories", {
3682
4072
  body: {
3683
- text,
4073
+ text: text2,
3684
4074
  type: kind,
3685
4075
  content: "{}",
3686
4076
  confidence: 1,
@@ -3821,13 +4211,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3821
4211
  }
3822
4212
 
3823
4213
  // src/commands/onboard.ts
3824
- import { existsSync as existsSync10 } from "fs";
3825
- 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";
3826
4216
 
3827
4217
  // src/commands/fanout.ts
3828
4218
  import { spawnSync } from "child_process";
3829
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3830
- 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";
3831
4221
  var ICON = {
3832
4222
  refresh: "\u21BB",
3833
4223
  bind: "+",
@@ -3847,21 +4237,21 @@ function discoverChildren(root) {
3847
4237
  const out = [];
3848
4238
  for (const name of names.sort()) {
3849
4239
  if (name.startsWith(".") || name === "node_modules") continue;
3850
- const dir = join11(root, name);
4240
+ const dir = join12(root, name);
3851
4241
  try {
3852
4242
  if (!statSync3(dir).isDirectory()) continue;
3853
4243
  } catch {
3854
4244
  continue;
3855
4245
  }
3856
- if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
4246
+ if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3857
4247
  }
3858
4248
  return out;
3859
4249
  }
3860
4250
  function readManifest(path) {
3861
- if (!existsSync9(path)) return null;
4251
+ if (!existsSync10(path)) return null;
3862
4252
  let parsed;
3863
4253
  try {
3864
- parsed = JSON.parse(readFileSync7(path, "utf8"));
4254
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
3865
4255
  } catch (err2) {
3866
4256
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3867
4257
  }
@@ -4045,28 +4435,17 @@ async function pickWorkspace(client, opts = {}) {
4045
4435
  if (candidates.length === 0) candidates = all;
4046
4436
  const dirToks = new Set(nameTokens(dirName));
4047
4437
  const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
4048
- const suggestions = candidates.filter(isMatch);
4049
- let pool = candidates;
4050
- if (candidates.length > 12 && suggestions.length === 0) {
4051
- const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
4052
- if (q) {
4053
- const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
4054
- if (hits.length > 0) pool = hits;
4055
- else process.stderr.write(`no match for "${q}" \u2014 listing all
4056
- `);
4057
- }
4058
- }
4059
4438
  const SKIP = "__skip__";
4060
4439
  const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
4061
- const matched = pool.filter(isMatch).sort(byPath);
4062
- const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
4440
+ const matched = candidates.filter(isMatch).sort(byPath);
4441
+ const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
4063
4442
  const choices = [
4064
4443
  ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
4065
4444
  ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
4066
4445
  { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
4067
4446
  ];
4068
4447
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
4069
- const chosen = await promptSelect(promptLabel, choices, defaultValue);
4448
+ const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
4070
4449
  if (chosen === SKIP) return void 0;
4071
4450
  const picked = byId.get(chosen);
4072
4451
  const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
@@ -4238,10 +4617,10 @@ async function chooseScope(scopeFlag, yes) {
4238
4617
  }
4239
4618
  async function planRecurseChild(entry, root, client, opts) {
4240
4619
  const dir = resolveChildDir(entry.path, root);
4241
- if (!existsSync10(dir)) {
4620
+ if (!existsSync11(dir)) {
4242
4621
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4243
4622
  }
4244
- if (existsSync10(join12(dir, ".sechroom.json"))) {
4623
+ if (existsSync11(join13(dir, ".sechroom.json"))) {
4245
4624
  return {
4246
4625
  label: entry.path,
4247
4626
  dir,
@@ -4314,7 +4693,7 @@ This fan-out will pin the same lane in every repo:
4314
4693
  async function runRecurse(cfg, g, opts) {
4315
4694
  const { yes, dryRun, json } = opts;
4316
4695
  const root = process.cwd();
4317
- const manifestPath = join12(root, ".sechroom", "repos.json");
4696
+ const manifestPath = join13(root, ".sechroom", "repos.json");
4318
4697
  const fromManifest = readManifest(manifestPath);
4319
4698
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4320
4699
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4826,23 +5205,23 @@ Examples:
4826
5205
 
4827
5206
  // src/commands/reset.ts
4828
5207
  import { homedir as homedir4 } from "os";
4829
- import { join as join13 } from "path";
4830
- 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";
4831
5210
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4832
- var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
4833
- var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
4834
- var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
4835
- 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");
4836
5215
  function removeMaterialisedSkills(dir) {
4837
5216
  const removed = [];
4838
- const lockPath = join13(dir, SKILLS_LOCK2);
4839
- if (!existsSync11(lockPath)) return removed;
5217
+ const lockPath = join14(dir, SKILLS_LOCK2);
5218
+ if (!existsSync12(lockPath)) return removed;
4840
5219
  try {
4841
- const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
5220
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
4842
5221
  for (const entry of Object.values(lock)) {
4843
5222
  for (const name of entry.skills ?? []) {
4844
- const p = join13(dir, name);
4845
- if (existsSync11(p)) {
5223
+ const p = join14(dir, name);
5224
+ if (existsSync12(p)) {
4846
5225
  rmSync3(p, { recursive: true, force: true });
4847
5226
  removed.push(p);
4848
5227
  }
@@ -4887,18 +5266,18 @@ function registerReset(program2) {
4887
5266
  }
4888
5267
  }
4889
5268
  const removed = [];
4890
- const stateDir = join13(process.cwd(), ".sechroom");
4891
- if (existsSync11(stateDir)) {
5269
+ const stateDir = join14(process.cwd(), ".sechroom");
5270
+ if (existsSync12(stateDir)) {
4892
5271
  rmSync3(stateDir, { recursive: true, force: true });
4893
5272
  removed.push(stateDir);
4894
5273
  }
4895
- const legacyCfg = join13(process.cwd(), ".sechroom.json");
4896
- if (existsSync11(legacyCfg)) {
5274
+ const legacyCfg = join14(process.cwd(), ".sechroom.json");
5275
+ if (existsSync12(legacyCfg)) {
4897
5276
  rmSync3(legacyCfg, { force: true });
4898
5277
  removed.push(legacyCfg);
4899
5278
  }
4900
- const legacySem = join13(process.cwd(), ".sem");
4901
- if (existsSync11(legacySem)) {
5279
+ const legacySem = join14(process.cwd(), ".sem");
5280
+ if (existsSync12(legacySem)) {
4902
5281
  rmSync3(legacySem, { force: true });
4903
5282
  removed.push(legacySem);
4904
5283
  }
@@ -4924,8 +5303,8 @@ function registerReset(program2) {
4924
5303
  }
4925
5304
 
4926
5305
  // src/commands/skills.ts
4927
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
4928
- 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";
4929
5308
  function filenameFromDisposition(header) {
4930
5309
  if (!header) return void 0;
4931
5310
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -4933,11 +5312,11 @@ function filenameFromDisposition(header) {
4933
5312
  }
4934
5313
  function resolveOutputPath(output, serverFilename) {
4935
5314
  const filename = serverFilename || "skills.zip";
4936
- if (!output) return join14(process.cwd(), filename);
4937
- 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();
4938
5317
  if (looksLikeDir) {
4939
- mkdirSync10(output, { recursive: true });
4940
- return join14(output, filename);
5318
+ mkdirSync11(output, { recursive: true });
5319
+ return join15(output, filename);
4941
5320
  }
4942
5321
  return output;
4943
5322
  }
@@ -4968,7 +5347,7 @@ async function downloadZip(label, call, output) {
4968
5347
  const buf = Buffer.from(res.data);
4969
5348
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
4970
5349
  const path = resolveOutputPath(output, filename);
4971
- writeFileSync10(path, buf);
5350
+ writeFileSync11(path, buf);
4972
5351
  return { path, bytes: buf.length, filename };
4973
5352
  }
4974
5353
  function registerSkills(program2) {
@@ -5136,12 +5515,12 @@ Examples:
5136
5515
  }
5137
5516
 
5138
5517
  // src/commands/sweep.ts
5139
- import { existsSync as existsSync13 } from "fs";
5140
- import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
5141
- 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");
5142
5521
  function planEntry(entry, root) {
5143
5522
  const dir = resolveChildDir(entry.path, root);
5144
- if (!existsSync13(dir)) {
5523
+ if (!existsSync14(dir)) {
5145
5524
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5146
5525
  }
5147
5526
  if (committedBindingPath(dir)) {
@@ -5217,7 +5596,7 @@ Examples:
5217
5596
  `);
5218
5597
  return;
5219
5598
  }
5220
- const root = dirname8(dirname8(manifestPath));
5599
+ const root = dirname9(dirname9(manifestPath));
5221
5600
  const plans = repos.map((entry) => planEntry(entry, root));
5222
5601
  if (!json) {
5223
5602
  process.stderr.write(
@@ -5236,13 +5615,13 @@ Examples:
5236
5615
 
5237
5616
  // src/commands/telemetry.ts
5238
5617
  import {
5239
- existsSync as existsSync14,
5240
- mkdirSync as mkdirSync11,
5241
- readFileSync as readFileSync9,
5618
+ existsSync as existsSync15,
5619
+ mkdirSync as mkdirSync12,
5620
+ readFileSync as readFileSync11,
5242
5621
  rmSync as rmSync4,
5243
- writeFileSync as writeFileSync11
5622
+ writeFileSync as writeFileSync12
5244
5623
  } from "fs";
5245
- import { dirname as dirname9, join as join16 } from "path";
5624
+ import { dirname as dirname10, join as join17 } from "path";
5246
5625
  function registerTelemetry(program2) {
5247
5626
  const telemetry = program2.command("telemetry").description(
5248
5627
  "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
@@ -5312,14 +5691,14 @@ function registerTelemetry(program2) {
5312
5691
  "Decomposition id this session executes"
5313
5692
  ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5314
5693
  const json = Boolean(cmd.optsWithGlobals().json);
5315
- const dir = join16(process.cwd(), ".sechroom");
5316
- mkdirSync11(dir, { recursive: true });
5317
- 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);
5318
5697
  const binding = {
5319
5698
  decompositionId: opts.decomposition,
5320
5699
  taskId: opts.task
5321
5700
  };
5322
- writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
5701
+ writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
5323
5702
  ensureStateDirIgnored(process.cwd());
5324
5703
  if (json) {
5325
5704
  emit({ bound: true, ...binding, path }, true);
@@ -5334,8 +5713,8 @@ function registerTelemetry(program2) {
5334
5713
  });
5335
5714
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5336
5715
  const json = Boolean(cmd.optsWithGlobals().json);
5337
- const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
5338
- const existed = existsSync14(path);
5716
+ const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
5717
+ const existed = existsSync15(path);
5339
5718
  if (existed) rmSync4(path);
5340
5719
  if (json) emit({ unbound: existed, path }, true);
5341
5720
  else
@@ -5449,11 +5828,11 @@ async function postTelemetry(cfg, decompositionId, events) {
5449
5828
  function findBinding(start) {
5450
5829
  let dir = start;
5451
5830
  for (; ; ) {
5452
- const path = join16(dir, ".sechroom", BINDING_FILE);
5453
- if (existsSync14(path)) {
5831
+ const path = join17(dir, ".sechroom", BINDING_FILE);
5832
+ if (existsSync15(path)) {
5454
5833
  try {
5455
5834
  const b = JSON.parse(
5456
- readFileSync9(path, "utf8")
5835
+ readFileSync11(path, "utf8")
5457
5836
  );
5458
5837
  if (b.decompositionId && b.taskId)
5459
5838
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5461,18 +5840,18 @@ function findBinding(start) {
5461
5840
  }
5462
5841
  return null;
5463
5842
  }
5464
- const parent = dirname9(dir);
5843
+ const parent = dirname10(dir);
5465
5844
  if (parent === dir) return null;
5466
5845
  dir = parent;
5467
5846
  }
5468
5847
  }
5469
5848
  function parseTranscript(path) {
5470
- if (!existsSync14(path)) return null;
5849
+ if (!existsSync15(path)) return null;
5471
5850
  let tokensIn = 0;
5472
5851
  let tokensOut = 0;
5473
5852
  let contextUsed = 0;
5474
5853
  let model = "";
5475
- for (const line of readFileSync9(path, "utf8").split("\n")) {
5854
+ for (const line of readFileSync11(path, "utf8").split("\n")) {
5476
5855
  if (!line.trim()) continue;
5477
5856
  let obj;
5478
5857
  try {
@@ -5489,11 +5868,12 @@ function parseTranscript(path) {
5489
5868
  if (obj.message?.model) model = obj.message.model;
5490
5869
  }
5491
5870
  if (tokensIn === 0 && tokensOut === 0) return null;
5492
- return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
5871
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
5493
5872
  }
5494
- function windowFor(model) {
5873
+ function windowFor(model, contextUsed = 0) {
5495
5874
  const m = model.toLowerCase();
5496
- 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;
5497
5877
  }
5498
5878
  async function readStdin2() {
5499
5879
  if (process.stdin.isTTY) return "";
@@ -5729,7 +6109,7 @@ Examples:
5729
6109
  function resolveVersion() {
5730
6110
  try {
5731
6111
  const pkg = JSON.parse(
5732
- readFileSync10(new URL("../package.json", import.meta.url), "utf8")
6112
+ readFileSync12(new URL("../package.json", import.meta.url), "utf8")
5733
6113
  );
5734
6114
  return pkg.version ?? "0.0.0";
5735
6115
  } catch {
@@ -5887,6 +6267,8 @@ registerLookup(program);
5887
6267
  registerRelationships(program);
5888
6268
  registerWorkspace(program);
5889
6269
  registerProject(program);
6270
+ registerDecomposition(program);
6271
+ registerClose(program);
5890
6272
  registerFiling(program);
5891
6273
  registerContinuity(program);
5892
6274
  registerCheckpoint(program);