@sechroom/cli 2026.7.18 → 2026.7.19-rc.82de7558

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 (3) hide show
  1. package/README.md +43 -19
  2. package/dist/index.js +1303 -1129
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -657,9 +657,14 @@ async function runApi(label, fn) {
657
657
  s.succeed();
658
658
  return res.data;
659
659
  }
660
- function fail(error) {
660
+ function formatFailureMessage(error) {
661
661
  let msg;
662
- if (typeof error === "object" && error !== null && "title" in error) {
662
+ if (error instanceof Error) {
663
+ msg = error.message || error.name;
664
+ if (error.cause instanceof Error && error.cause.message) {
665
+ msg += `: ${error.cause.message}`;
666
+ }
667
+ } else if (typeof error === "object" && error !== null && "title" in error) {
663
668
  const problem = error;
664
669
  msg = String(problem.title);
665
670
  if (problem.errors && typeof problem.errors === "object") {
@@ -674,6 +679,10 @@ ${detail.join("\n")}`;
674
679
  } else {
675
680
  msg = String(error);
676
681
  }
682
+ return msg;
683
+ }
684
+ function fail(error) {
685
+ const msg = formatFailureMessage(error);
677
686
  process.stderr.write(`error: ${msg}
678
687
  `);
679
688
  process.exit(1);
@@ -1509,8 +1518,8 @@ target:gpt-codex-agent), the dispatchable workers your loop skills call
1509
1518
  }
1510
1519
 
1511
1520
  // src/commands/channel.ts
1512
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1513
- import { dirname as dirname4, join as join7 } from "path";
1521
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
1522
+ import { dirname as dirname6, join as join9 } from "path";
1514
1523
  import {
1515
1524
  HttpTransportType,
1516
1525
  HubConnectionBuilder
@@ -1518,52 +1527,218 @@ import {
1518
1527
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1519
1528
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1520
1529
 
1530
+ // src/commands/executor.ts
1531
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
1532
+ import { dirname as dirname5, join as join8 } from "path";
1533
+
1534
+ // src/sem.ts
1535
+ import { dirname as dirname2, join as join5 } from "path";
1536
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1537
+ var SEM_FILE = join5(".sechroom", "lane.json");
1538
+ var STATE_DIR_NAME2 = ".sechroom";
1539
+ function localSemPath(cwd = process.cwd()) {
1540
+ return join5(cwd, SEM_FILE);
1541
+ }
1542
+ function resolveSemPathForRead(start = process.cwd()) {
1543
+ let dir = start;
1544
+ while (true) {
1545
+ const candidate = join5(dir, SEM_FILE);
1546
+ if (existsSync4(candidate)) return candidate;
1547
+ const parent = dirname2(dir);
1548
+ if (parent === dir) return void 0;
1549
+ dir = parent;
1550
+ }
1551
+ }
1552
+ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1553
+ try {
1554
+ let dir = start;
1555
+ let gitPath;
1556
+ for (; ; ) {
1557
+ const candidate = join5(dir, ".git");
1558
+ if (existsSync4(candidate)) {
1559
+ gitPath = candidate;
1560
+ break;
1561
+ }
1562
+ const parent = dirname2(dir);
1563
+ if (parent === dir) break;
1564
+ dir = parent;
1565
+ }
1566
+ if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1567
+ const gitFile = readFileSync3(gitPath, "utf8");
1568
+ const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1569
+ if (!common) return lane;
1570
+ const worktreesDir = join5(common[1], "worktrees");
1571
+ const siblings = readdirSync(worktreesDir).filter((n) => {
1572
+ try {
1573
+ return statSync(join5(worktreesDir, n)).isDirectory();
1574
+ } catch {
1575
+ return false;
1576
+ }
1577
+ });
1578
+ return laneWithWorktreeSuffix(lane, gitFile, siblings);
1579
+ } catch {
1580
+ return lane;
1581
+ }
1582
+ }
1583
+ function laneWithWorktreeSuffix(lane, gitFile, siblings) {
1584
+ const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
1585
+ if (!m) return lane;
1586
+ const idx = [...siblings].sort().indexOf(m[1]);
1587
+ return idx < 0 ? lane : `${lane}-${idx + 2}`;
1588
+ }
1589
+ function serializeSem(values) {
1590
+ return JSON.stringify(values, null, 2) + "\n";
1591
+ }
1592
+ function readSem(path) {
1593
+ const p = path ?? resolveSemPathForRead();
1594
+ if (!p || !existsSync4(p)) return void 0;
1595
+ return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
1596
+ }
1597
+ function readLocalSemValues(cwd = process.cwd()) {
1598
+ const next = join5(cwd, SEM_FILE);
1599
+ if (existsSync4(next)) return readSem(next)?.values ?? {};
1600
+ return {};
1601
+ }
1602
+ function parseLaneJson(text2) {
1603
+ try {
1604
+ const parsed = JSON.parse(text2);
1605
+ const out = {};
1606
+ for (const [k, v] of Object.entries(parsed)) {
1607
+ if (typeof v === "string") out[k] = v;
1608
+ }
1609
+ return out;
1610
+ } catch {
1611
+ return {};
1612
+ }
1613
+ }
1614
+ var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1615
+ function writeSem(values, path = localSemPath()) {
1616
+ mkdirSync4(dirname2(path), { recursive: true });
1617
+ writeFileSync4(path, serializeSem(values));
1618
+ ensureSemIgnored(path);
1619
+ ensureContinuityScaffold(path);
1620
+ return path;
1621
+ }
1622
+ function ensureStateDirIgnored(cwd = process.cwd()) {
1623
+ ensureSemIgnored(localSemPath(cwd));
1624
+ }
1625
+ var CONTINUITY_FILE_NAME = "continuity.json";
1626
+ var CONTINUITY_SCAFFOLD = JSON.stringify(
1627
+ {
1628
+ _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.",
1629
+ objective: "",
1630
+ state: "",
1631
+ lastAction: "",
1632
+ nextAction: "",
1633
+ resumeInstruction: "",
1634
+ constraints: [],
1635
+ questions: [],
1636
+ artifacts: [],
1637
+ confidence: null
1638
+ },
1639
+ null,
1640
+ 2
1641
+ ) + "\n";
1642
+ function ensureContinuityScaffold(semPath) {
1643
+ try {
1644
+ const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
1645
+ if (existsSync4(target)) return;
1646
+ writeFileSync4(target, CONTINUITY_SCAFFOLD);
1647
+ } catch {
1648
+ }
1649
+ }
1650
+ function ignoresSem(content) {
1651
+ return content.split("\n").some((line) => {
1652
+ const t = line.trim();
1653
+ 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}`;
1654
+ });
1655
+ }
1656
+ function inGitRepo(startDir) {
1657
+ let dir = startDir;
1658
+ for (; ; ) {
1659
+ if (existsSync4(join5(dir, ".git"))) return true;
1660
+ const parent = dirname2(dir);
1661
+ if (parent === dir) return false;
1662
+ dir = parent;
1663
+ }
1664
+ }
1665
+ function resolveGitignoreTarget(startDir) {
1666
+ let dir = startDir;
1667
+ for (; ; ) {
1668
+ const gi = join5(dir, ".gitignore");
1669
+ if (existsSync4(gi)) return { path: gi, exists: true };
1670
+ const parent = dirname2(dir);
1671
+ if (existsSync4(join5(dir, ".git")) || parent === dir) {
1672
+ return { path: join5(startDir, ".gitignore"), exists: false };
1673
+ }
1674
+ dir = parent;
1675
+ }
1676
+ }
1677
+ function ensureSemIgnored(semPath) {
1678
+ try {
1679
+ const checkoutDir = dirname2(dirname2(semPath));
1680
+ if (!inGitRepo(checkoutDir)) return;
1681
+ const target = resolveGitignoreTarget(checkoutDir);
1682
+ if (target.exists) {
1683
+ const content = readFileSync3(target.path, "utf8");
1684
+ if (ignoresSem(content)) return;
1685
+ const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1686
+ appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1687
+ `);
1688
+ } else {
1689
+ writeFileSync4(target.path, `${STATE_DIR_IGNORE}
1690
+ `);
1691
+ }
1692
+ } catch {
1693
+ }
1694
+ }
1695
+
1521
1696
  // src/commands/hook-install.ts
1522
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1523
- import { delimiter, dirname as dirname3, join as join6 } from "path";
1697
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1698
+ import { delimiter, dirname as dirname4, join as join7 } from "path";
1524
1699
 
1525
1700
  // src/setup/clients.ts
1526
- import { existsSync as existsSync4 } from "fs";
1701
+ import { existsSync as existsSync5 } from "fs";
1527
1702
  import { homedir as homedir3 } from "os";
1528
- import { dirname as dirname2, join as join5 } from "path";
1703
+ import { dirname as dirname3, join as join6 } from "path";
1529
1704
  function claudeDesktopConfigPath(home) {
1530
1705
  switch (process.platform) {
1531
1706
  case "darwin":
1532
- return join5(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1707
+ return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1533
1708
  case "win32":
1534
- return join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1709
+ return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1535
1710
  default:
1536
- return join5(home, ".config", "Claude", "claude_desktop_config.json");
1711
+ return join6(home, ".config", "Claude", "claude_desktop_config.json");
1537
1712
  }
1538
1713
  }
1539
1714
  function clientTargets(cwd, opts = {}) {
1540
1715
  const home = homedir3();
1541
- const claudeDir = opts.claudeDir ?? join5(home, ".claude");
1542
- const codexHome = opts.codexHome ?? join5(home, ".codex");
1716
+ const claudeDir = opts.claudeDir ?? join6(home, ".claude");
1717
+ const codexHome = opts.codexHome ?? join6(home, ".codex");
1543
1718
  return {
1544
1719
  "claude-code": {
1545
1720
  key: "claude-code",
1546
1721
  label: "Claude Code",
1547
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".mcp.json"), format: "json" },
1548
- instruction: { surfaceKey: "claude-code", path: join5(cwd, "CLAUDE.md") }
1722
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
1723
+ instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
1549
1724
  },
1550
1725
  "claude-desktop": {
1551
1726
  key: "claude-desktop",
1552
1727
  label: "Claude Desktop",
1553
1728
  mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1554
- instruction: { surfaceKey: "claude-desktop", path: join5(claudeDir, "CLAUDE.md") }
1729
+ instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
1555
1730
  },
1556
1731
  codex: {
1557
1732
  key: "codex",
1558
1733
  label: "Codex CLI",
1559
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join5(codexHome, "config.toml"), format: "toml" },
1560
- instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1734
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
1735
+ instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1561
1736
  },
1562
1737
  cursor: {
1563
1738
  key: "cursor",
1564
1739
  label: "Cursor",
1565
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".cursor", "mcp.json"), format: "json" },
1566
- instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1740
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
1741
+ instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1567
1742
  },
1568
1743
  antigravity: {
1569
1744
  key: "antigravity",
@@ -1574,8 +1749,8 @@ function clientTargets(cwd, opts = {}) {
1574
1749
  // `type` — comes from the `antigravity` server surface, so we don't
1575
1750
  // hardcode it here. Instructions go in the project `AGENTS.md`
1576
1751
  // (cross-tool, shared with Codex/Cursor).
1577
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join5(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1578
- instruction: { surfaceKey: "antigravity", path: join5(cwd, "AGENTS.md") }
1752
+ mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1753
+ instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
1579
1754
  }
1580
1755
  };
1581
1756
  }
@@ -1584,11 +1759,11 @@ var DEFAULT_CLIENT_KEY = "claude-code";
1584
1759
  function detectInstalledClients(cwd) {
1585
1760
  const home = homedir3();
1586
1761
  const detected = [];
1587
- if (resolveClaudeTargets({}).some((t) => existsSync4(t.dir))) detected.push("claude-code");
1588
- if (existsSync4(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1589
- if (resolveCodexHomes({}).some((d) => existsSync4(d))) detected.push("codex");
1590
- if (existsSync4(join5(home, ".cursor")) || existsSync4(join5(cwd, ".cursor"))) detected.push("cursor");
1591
- if (existsSync4(join5(home, ".gemini"))) detected.push("antigravity");
1762
+ if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
1763
+ if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1764
+ if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
1765
+ if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
1766
+ if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
1592
1767
  return detected;
1593
1768
  }
1594
1769
 
@@ -1631,29 +1806,29 @@ function mergeHooks(config2, commands) {
1631
1806
  return added;
1632
1807
  }
1633
1808
  function readJsonConfig2(path) {
1634
- if (!existsSync5(path)) return {};
1635
- const raw = readFileSync3(path, "utf8");
1809
+ if (!existsSync6(path)) return {};
1810
+ const raw = readFileSync4(path, "utf8");
1636
1811
  if (!raw.trim()) return {};
1637
1812
  return JSON.parse(raw);
1638
1813
  }
1639
1814
  function installHooksJson(path, commands, dryRun) {
1640
- const existed = existsSync5(path) && readFileSync3(path, "utf8").trim().length > 0;
1815
+ const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
1641
1816
  const config2 = readJsonConfig2(path);
1642
1817
  const added = mergeHooks(config2, commands);
1643
1818
  if (added === 0 && existed) return { path, status: "current" };
1644
1819
  if (!dryRun) {
1645
- mkdirSync4(dirname3(path), { recursive: true });
1646
- writeFileSync4(path, JSON.stringify(config2, null, 2) + "\n");
1820
+ mkdirSync5(dirname4(path), { recursive: true });
1821
+ writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1647
1822
  }
1648
1823
  return { path, status: existed ? "merged" : "created" };
1649
1824
  }
1650
1825
  function installClaudeCommands(claudeDir, commands, dryRun) {
1651
- return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
1826
+ return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
1652
1827
  }
1653
1828
  function installCodexCommands(codexHome, commands, dryRun) {
1654
1829
  return [
1655
- installHooksJson(join6(codexHome, "hooks.json"), commands, dryRun),
1656
- installCodexFeatureFlag(join6(codexHome, "config.toml"), dryRun)
1830
+ installHooksJson(join7(codexHome, "hooks.json"), commands, dryRun),
1831
+ installCodexFeatureFlag(join7(codexHome, "config.toml"), dryRun)
1657
1832
  ];
1658
1833
  }
1659
1834
  function ensureCodexFeaturesHooks(content) {
@@ -1677,13 +1852,13 @@ function ensureCodexFeaturesHooks(content) {
1677
1852
  return { next: lines.join("\n"), changed: true };
1678
1853
  }
1679
1854
  function installCodexFeatureFlag(path, dryRun) {
1680
- const existed = existsSync5(path);
1681
- const content = existed ? readFileSync3(path, "utf8") : "";
1855
+ const existed = existsSync6(path);
1856
+ const content = existed ? readFileSync4(path, "utf8") : "";
1682
1857
  const { next, changed } = ensureCodexFeaturesHooks(content);
1683
1858
  if (!changed) return { path, status: "current" };
1684
1859
  if (!dryRun) {
1685
- mkdirSync4(dirname3(path), { recursive: true });
1686
- writeFileSync4(path, next);
1860
+ mkdirSync5(dirname4(path), { recursive: true });
1861
+ writeFileSync5(path, next);
1687
1862
  }
1688
1863
  return { path, status: existed ? "merged" : "created" };
1689
1864
  }
@@ -1708,11 +1883,11 @@ function installHookSurfaces(surfaces, opts) {
1708
1883
  const out = [];
1709
1884
  for (const surface of surfaces) {
1710
1885
  if (surface === "claude") {
1711
- const path = join6(opts.claudeDir, "settings.json");
1886
+ const path = join7(opts.claudeDir, "settings.json");
1712
1887
  out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1713
1888
  } else {
1714
- const hooksJson = installHooksJson(join6(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1715
- const featureFlag = installCodexFeatureFlag(join6(opts.codexHome, "config.toml"), opts.dryRun);
1889
+ const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1890
+ const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
1716
1891
  out.push({ surface, results: [hooksJson, featureFlag] });
1717
1892
  }
1718
1893
  }
@@ -1732,7 +1907,7 @@ function isSechroomOnPath() {
1732
1907
  for (const dir of pathEnv.split(delimiter)) {
1733
1908
  if (!dir) continue;
1734
1909
  for (const name of names) {
1735
- if (existsSync5(join6(dir, name))) return true;
1910
+ if (existsSync6(join7(dir, name))) return true;
1736
1911
  }
1737
1912
  }
1738
1913
  return false;
@@ -1745,619 +1920,901 @@ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1745
1920
  return true;
1746
1921
  }
1747
1922
 
1748
- // src/commands/channel.ts
1749
- function registerChannel(program2) {
1750
- const channel = program2.command("channel").description(
1751
- "Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
1923
+ // src/commands/executor.ts
1924
+ function executorSubscriptionInput(name) {
1925
+ return {
1926
+ name,
1927
+ enabled: true,
1928
+ filter: { tags: ["kind:task"], workspaceScope: [] }
1929
+ };
1930
+ }
1931
+ function executorRegistrationInput(state, deliverySubscriptionId) {
1932
+ return {
1933
+ relayId: state.relayId,
1934
+ instanceKey: state.instanceKey,
1935
+ laneId: state.laneId ?? state.instanceKey,
1936
+ runtimeKind: parseRuntimeKind(state.runtime),
1937
+ activationMode: "Attached",
1938
+ deliverySubscriptionId,
1939
+ connectorId: state.connectorId,
1940
+ claimedCapabilityKeys: state.capabilityKeys,
1941
+ toolSetRef: null,
1942
+ ttlSeconds: state.ttlSeconds
1943
+ };
1944
+ }
1945
+ var EXECUTOR_STATE = "executor.json";
1946
+ var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
1947
+ var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
1948
+ var CLAUDE_EXECUTOR_HOOKS = {
1949
+ SessionStart: EXECUTOR_PULSE_COMMAND,
1950
+ UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
1951
+ PreToolUse: EXECUTOR_PULSE_COMMAND,
1952
+ Stop: EXECUTOR_PULSE_COMMAND,
1953
+ SessionEnd: EXECUTOR_STOP_COMMAND
1954
+ };
1955
+ var CODEX_EXECUTOR_HOOKS = {
1956
+ SessionStart: EXECUTOR_PULSE_COMMAND,
1957
+ UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
1958
+ PreToolUse: EXECUTOR_PULSE_COMMAND,
1959
+ Stop: EXECUTOR_PULSE_COMMAND
1960
+ };
1961
+ function registerExecutor(program2) {
1962
+ const executor = program2.command("executor").description(
1963
+ "Register and operate a local Claude Code/Codex executor advertisement"
1752
1964
  );
1753
- const withFilterOpts = (c) => c.option(
1754
- "--name <name>",
1755
- "Subscription name (idempotent per name)",
1756
- "wlp-dispatch"
1965
+ executor.command("install").description(
1966
+ "Configure this checkout's harness to advertise itself as a WLP executor"
1967
+ ).option("--connector <id>", "Approved local-session ConnectorDefinition id").option(
1968
+ "--instance-key <key>",
1969
+ "Stable executor identity (defaults to .sechroom/lane.json code-lane)"
1757
1970
  ).option(
1758
- "--tag <tag...>",
1759
- "Tag(s) the event must carry to match (repeatable). Default targets WLP task dispatches.",
1760
- ["kind:task"]
1761
- ).option("--workspace <wsp...>", "Restrict to these workspace id(s)").option(
1762
- "--executor-instance <id>",
1763
- "Join the exact executor-instance SignalR group"
1764
- );
1765
- withFilterOpts(
1766
- channel.command("connect").description(
1767
- "Register a SignalR subscription and stream matched events to stdout"
1768
- )
1769
- ).action(async (opts, cmd) => {
1770
- const json = Boolean(cmd.optsWithGlobals().json);
1771
- const cfg = resolveConfig(cmd.optsWithGlobals());
1772
- const filter = readFilter(opts);
1773
- const sub = await ensureSubscription(cfg, opts.name, filter);
1774
- const seen = /* @__PURE__ */ new Set();
1775
- const deliver = makeDeliver(
1776
- filter,
1777
- seen,
1778
- (payload) => process.stdout.write(
1779
- (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
1780
- )
1781
- );
1782
- const conn = await openConnection(cfg, deliver, opts.executorInstance);
1783
- await reconcile(cfg, filter, deliver);
1784
- if (json) {
1785
- emit(
1786
- {
1787
- connected: true,
1788
- tenant: cfg.tenant,
1789
- subscriptionId: sub.id ?? opts.name,
1790
- filter
1791
- },
1792
- true
1971
+ "--lane-id <lane>",
1972
+ "Canonical affinity lane (defaults to .sechroom/lane.json code-lane)"
1973
+ ).option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option(
1974
+ "--capability <key...>",
1975
+ "Capability operation keys claimed by this instance"
1976
+ ).option(
1977
+ "--relay <id>",
1978
+ "Relay identity shared by sibling instances",
1979
+ "sechroom-cli-local"
1980
+ ).option(
1981
+ "--subscription-name <name>",
1982
+ "SignalR delivery binding name",
1983
+ "executor-dispatch"
1984
+ ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option(
1985
+ "--refresh-after <seconds>",
1986
+ "Minimum age before a hook refreshes",
1987
+ parseInteger,
1988
+ 40
1989
+ ).option(
1990
+ "-y, --yes",
1991
+ "Non-interactive: accept detected surface and lane defaults",
1992
+ false
1993
+ ).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
1994
+ const globals = cmd.optsWithGlobals();
1995
+ const lane = readSem()?.values["code-lane"];
1996
+ const detected = detectHookSurfaces(process.cwd());
1997
+ let surface = opts.surface;
1998
+ let instanceKey = opts.instanceKey;
1999
+ let runtime = opts.runtime;
2000
+ let laneId = opts.laneId;
2001
+ let connector = opts.connector;
2002
+ let capabilities = opts.capability;
2003
+ const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
2004
+ if (!opts.yes && canPrompt()) {
2005
+ surface = await promptText(
2006
+ "Harness surface (claude or codex)?",
2007
+ surface ?? surfaceDefault
1793
2008
  );
1794
- } else {
1795
- process.stderr.write(
1796
- style.green("channel connected") + style.dim(
1797
- ` \u2014 tenant ${cfg.tenant}, sub ${sub.id ?? opts.name}, tags [${filter.tags.join(", ")}]
1798
- `
1799
- ) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
2009
+ instanceKey = await promptText(
2010
+ "Executor instance key?",
2011
+ instanceKey ?? lane ?? ""
2012
+ );
2013
+ laneId = await promptText(
2014
+ "Executor affinity lane?",
2015
+ laneId ?? lane ?? ""
2016
+ );
2017
+ runtime = await promptText(
2018
+ "Runtime (claude-code or codex)?",
2019
+ runtime ?? (surface === "codex" ? "codex" : "claude-code")
1800
2020
  );
2021
+ connector = await promptText(
2022
+ "Approved local-session connector id?",
2023
+ connector ?? ""
2024
+ );
2025
+ const capabilityText = await promptText(
2026
+ "Capability keys (comma-separated; blank for none)?",
2027
+ capabilities?.join(",") ?? ""
2028
+ );
2029
+ capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
1801
2030
  }
1802
- await holdOpen(conn);
1803
- });
1804
- withFilterOpts(
1805
- channel.command("mcp").description(
1806
- "Run as a Claude Code channel (local-stdio MCP server) \u2014 push matched events into the session"
1807
- )
1808
- ).action(async (opts, cmd) => {
1809
- const cfg = resolveConfig(cmd.optsWithGlobals());
1810
- const filter = readFilter(opts);
1811
- const mcp = new Server(
1812
- { name: "sechroom", version: "0.1.0" },
1813
- {
1814
- capabilities: { experimental: { "claude/channel": {} } },
1815
- instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch (kind:task \u2192 status:in-progress) means a task is runnable now: load the memory id from the event, do the work, then write a closeout memory tagged wlp-decomposition:{id} + wlp-task:{taskId} + verdict:{pass|soft-fail|plan-invalid|blocked}.'
1816
- }
1817
- );
1818
- await mcp.connect(new StdioServerTransport());
1819
- await ensureSubscription(cfg, opts.name, filter);
1820
- const seen = /* @__PURE__ */ new Set();
1821
- const deliver = makeDeliver(filter, seen, (payload) => {
1822
- const { content, meta } = summarizeEvent(payload);
1823
- void mcp.notification({
1824
- method: "notifications/claude/channel",
1825
- params: { content, meta }
1826
- }).catch(
1827
- (e) => process.stderr.write(err(`channel push failed: ${String(e)}
1828
- `))
2031
+ surface ??= surfaceDefault;
2032
+ instanceKey ??= lane;
2033
+ laneId ??= lane;
2034
+ runtime ??= surface === "codex" ? "codex" : "claude-code";
2035
+ if (!connector)
2036
+ fail(
2037
+ "executor install requires --connector (or an interactive connector id)"
1829
2038
  );
1830
- });
1831
- const conn = await openConnection(cfg, deliver, opts.executorInstance);
1832
- await reconcile(cfg, filter, deliver);
1833
- process.stderr.write(
1834
- style.dim(
1835
- `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
1836
- `
1837
- )
1838
- );
1839
- await holdOpen(conn);
1840
- });
1841
- channel.command("install").description(
1842
- "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
1843
- ).option(
1844
- "--workspace <wsp...>",
1845
- "Restrict dispatches to these workspace id(s)"
1846
- ).option(
1847
- "--tag <tag...>",
1848
- "Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
1849
- ["kind:task"]
1850
- ).option(
1851
- "--name <name>",
1852
- "MCP server + subscription name (idempotent per name)",
1853
- "sechroom-channel"
1854
- ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
1855
- const path = join7(process.cwd(), ".mcp.json");
1856
- const dryRun = Boolean(opts.dryRun);
1857
- const args = ["channel", "mcp", "--name", opts.name];
1858
- for (const w of opts.workspace ?? [])
1859
- args.push("--workspace", w);
1860
- for (const t of opts.tag ?? []) args.push("--tag", t);
1861
- const entry = { command: "sechroom", args };
1862
- const config2 = readMcpConfig(path);
1863
- config2.mcpServers ??= {};
1864
- const existing = config2.mcpServers[opts.name];
1865
- const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
1866
- if (status !== "current" && !dryRun) {
1867
- config2.mcpServers[opts.name] = entry;
1868
- mkdirSync5(dirname4(path), { recursive: true });
1869
- writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
2039
+ if (!instanceKey)
2040
+ fail(
2041
+ "no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane"
2042
+ );
2043
+ if (!opts.yes && !canPrompt())
2044
+ fail("non-interactive executor install requires --yes");
2045
+ parseRuntimeKind(runtime);
2046
+ if (!["claude", "codex"].includes(surface))
2047
+ fail("surface must be claude or codex");
2048
+ if (opts.refreshAfter >= opts.ttl)
2049
+ fail("refresh-after must be shorter than the TTL");
2050
+ const sem = readSem();
2051
+ const checkout = sem ? dirname5(dirname5(sem.path)) : process.cwd();
2052
+ const statePath = join8(checkout, ".sechroom", EXECUTOR_STATE);
2053
+ const state = {
2054
+ schemaVersion: 1,
2055
+ instanceKey,
2056
+ laneId,
2057
+ runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
2058
+ connectorId: connector,
2059
+ capabilityKeys: capabilities ?? [],
2060
+ relayId: opts.relay,
2061
+ subscriptionName: opts.subscriptionName,
2062
+ ttlSeconds: opts.ttl,
2063
+ refreshAfterSeconds: opts.refreshAfter
2064
+ };
2065
+ if (!opts.dryRun) {
2066
+ mkdirSync6(dirname5(statePath), { recursive: true });
2067
+ writeFileSync6(statePath, JSON.stringify(state, null, 2) + "\n");
2068
+ ensureStateDirIgnored(checkout);
1870
2069
  }
1871
- const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
1872
- process.stdout.write(`${style.green("channel")} ${path} (${verb})
1873
- `);
1874
- process.stdout.write(
1875
- style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
2070
+ const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
2071
+ (target) => target.dir
2072
+ ) : [join8(checkout, ".claude")];
2073
+ const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join8(checkout, ".codex")];
2074
+ const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
2075
+ for (const target of hookTargets) {
2076
+ const results = surface === "claude" ? [
2077
+ installClaudeCommands(
2078
+ target,
2079
+ CLAUDE_EXECUTOR_HOOKS,
2080
+ opts.dryRun
2081
+ )
2082
+ ] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
2083
+ for (const result of results)
2084
+ process.stderr.write(describe(result, opts.dryRun) + "\n");
2085
+ }
2086
+ warnIfSechroomNotOnPath();
2087
+ process.stderr.write(
2088
+ style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
1876
2089
  `)
1877
2090
  );
1878
- if (status !== "current") {
1879
- process.stdout.write(
2091
+ });
2092
+ executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
2093
+ await drainStdin();
2094
+ const located = readExecutorState();
2095
+ if (!located) return;
2096
+ const { state, path } = located;
2097
+ const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
2098
+ if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
2099
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2100
+ try {
2101
+ await ensureExecutorInstance(cfg, { state, path });
2102
+ } catch {
2103
+ }
2104
+ });
2105
+ executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
2106
+ await drainStdin();
2107
+ const located = readExecutorState();
2108
+ if (!located?.state.instanceId) return;
2109
+ try {
2110
+ await api(
2111
+ resolveConfig(cmd.optsWithGlobals()),
2112
+ `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`,
2113
+ { method: "DELETE", body: JSON.stringify({}) }
2114
+ );
2115
+ delete located.state.instanceId;
2116
+ delete located.state.lastRefreshAt;
2117
+ writeFileSync6(
2118
+ located.path,
2119
+ JSON.stringify(located.state, null, 2) + "\n"
2120
+ );
2121
+ } catch {
2122
+ }
2123
+ });
2124
+ executor.command("submit-connector").description(
2125
+ "Submit a local-session connector definition for governed approval"
2126
+ ).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
2127
+ "base",
2128
+ "dotnet-10"
2129
+ ]).action(async (opts, cmd) => {
2130
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2131
+ const data = await api(cfg, "/connectors/definitions", {
2132
+ method: "POST",
2133
+ body: JSON.stringify({
2134
+ slug: opts.slug,
2135
+ displayName: opts.displayName,
2136
+ runtimeProfiles: opts.profile,
2137
+ connectorKind: "ExecutionRuntime",
2138
+ providerKind: "local-session",
2139
+ dispatchTransport: parseTransport(opts.transport)
2140
+ })
2141
+ });
2142
+ emit(data, Boolean(cmd.optsWithGlobals().json));
2143
+ if (!cmd.optsWithGlobals().json) {
2144
+ process.stderr.write(
1880
2145
  style.dim(
1881
- `
1882
- Load it (Channels research preview) by launching your agent with:
1883
- claude --dangerously-load-development-channels server:${opts.name}
1884
- `
2146
+ "approve this connector definition before registering executors\n"
1885
2147
  )
1886
2148
  );
1887
2149
  }
1888
- warnIfSechroomNotOnPath();
1889
2150
  });
1890
- channel.addHelpText(
1891
- "after",
1892
- `
1893
- Examples:
1894
- $ sechroom channel connect stream WLP task dispatches to stdout
1895
- $ sechroom channel connect --tag kind:task --tag status:in-progress
1896
- $ sechroom channel connect --workspace wsp_X --json | jq .
1897
-
1898
- # Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
1899
- $ sechroom channel install --workspace wsp_X --tag kind:task --tag status:in-progress
1900
- # then: claude --dangerously-load-development-channels server:sechroom-channel`
1901
- );
1902
- }
1903
- function readMcpConfig(path) {
1904
- if (!existsSync6(path)) return {};
1905
- const raw = readFileSync4(path, "utf8");
1906
- if (!raw.trim()) return {};
1907
- try {
1908
- return JSON.parse(raw);
1909
- } catch {
1910
- return fail(
1911
- `Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
2151
+ executor.command("register").description(
2152
+ "Create/reuse a SignalR binding and register this local executor instance"
2153
+ ).requiredOption(
2154
+ "--instance-key <key>",
2155
+ "Stable key for this concrete session/lane"
2156
+ ).requiredOption(
2157
+ "--connector <id>",
2158
+ "Approved local-session ConnectorDefinition id"
2159
+ ).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
2160
+ "--lane-id <lane>",
2161
+ "Canonical affinity lane (defaults to instance key)"
2162
+ ).option(
2163
+ "--relay <id>",
2164
+ "Relay identity shared by sibling instances",
2165
+ "sechroom-cli-local"
2166
+ ).option(
2167
+ "--subscription-name <name>",
2168
+ "SignalR delivery binding name",
2169
+ "executor-dispatch"
2170
+ ).option(
2171
+ "--capability <key...>",
2172
+ "Capability operation keys claimed by this instance"
2173
+ ).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
2174
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2175
+ const subscription = await api(
2176
+ cfg,
2177
+ "/me/delivery-subscriptions/signalr",
2178
+ {
2179
+ method: "POST",
2180
+ body: JSON.stringify({
2181
+ name: opts.subscriptionName,
2182
+ enabled: true,
2183
+ // Exact executor fanout ignores this generic filter; the fixed tag only
2184
+ // satisfies the legacy SignalR subscription shape.
2185
+ filter: { tags: ["kind:task"], workspaceScope: [] }
2186
+ })
2187
+ }
2188
+ );
2189
+ const data = await api(
2190
+ cfg,
2191
+ "/me/executor-instances",
2192
+ {
2193
+ method: "POST",
2194
+ body: JSON.stringify({
2195
+ relayId: opts.relay,
2196
+ instanceKey: opts.instanceKey,
2197
+ laneId: opts.laneId ?? opts.instanceKey,
2198
+ runtimeKind: parseRuntimeKind(opts.runtime),
2199
+ activationMode: "Attached",
2200
+ deliverySubscriptionId: subscription.id,
2201
+ connectorId: opts.connector,
2202
+ claimedCapabilityKeys: opts.capability ?? [],
2203
+ toolSetRef: opts.toolSetRef ?? null,
2204
+ ttlSeconds: opts.ttl
2205
+ })
2206
+ }
2207
+ );
2208
+ emit(data, Boolean(cmd.optsWithGlobals().json));
2209
+ if (!cmd.optsWithGlobals().json)
2210
+ process.stderr.write(
2211
+ style.dim(`refresh with: sechroom executor heartbeat ${data.id}
2212
+ `)
2213
+ );
2214
+ });
2215
+ executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
2216
+ const data = await refreshExecutorInstance(
2217
+ resolveConfig(cmd.optsWithGlobals()),
2218
+ id,
2219
+ opts.ttl
2220
+ );
2221
+ emit(data, Boolean(cmd.optsWithGlobals().json));
2222
+ });
2223
+ executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
2224
+ if (opts.interval >= opts.ttl)
2225
+ fail("heartbeat interval must be shorter than the TTL");
2226
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2227
+ await refreshExecutorInstance(cfg, id, opts.ttl);
2228
+ process.stderr.write(
2229
+ style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
2230
+ `)
1912
2231
  );
2232
+ await holdHeartbeat(async () => {
2233
+ await refreshExecutorInstance(cfg, id, opts.ttl);
2234
+ }, opts.interval * 1e3);
2235
+ });
2236
+ executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
2237
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2238
+ const data = await api(
2239
+ cfg,
2240
+ `/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
2241
+ );
2242
+ emit(data, Boolean(cmd.optsWithGlobals().json));
2243
+ });
2244
+ executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
2245
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2246
+ const data = await api(
2247
+ cfg,
2248
+ `/me/executor-instances/${encodeURIComponent(id)}`,
2249
+ {
2250
+ method: "DELETE",
2251
+ body: JSON.stringify({})
2252
+ }
2253
+ );
2254
+ emit(data, Boolean(cmd.optsWithGlobals().json));
2255
+ });
2256
+ }
2257
+ function parseRuntimeKind(value) {
2258
+ switch (value.trim().toLowerCase()) {
2259
+ case "claude":
2260
+ case "claude-code":
2261
+ return "ClaudeCode";
2262
+ case "codex":
2263
+ return "Codex";
2264
+ default:
2265
+ return fail("runtime must be claude-code or codex");
2266
+ }
2267
+ }
2268
+ function parseTransport(value) {
2269
+ switch (value.trim().toLowerCase()) {
2270
+ case "push":
2271
+ return "Push";
2272
+ case "pull":
2273
+ return "Pull";
2274
+ default:
2275
+ return fail("transport must be push or pull");
1913
2276
  }
1914
2277
  }
1915
- function readFilter(opts) {
1916
- const tags = opts.tag ?? [];
1917
- const workspaceScope = opts.workspace ?? [];
1918
- if (tags.length === 0 && workspaceScope.length === 0)
1919
- fail(
1920
- "A channel subscription needs at least one --tag or --workspace (an empty filter receives nothing)."
1921
- );
1922
- return { tags, workspaceScope };
2278
+ async function refreshExecutorInstance(cfg, id, ttlSeconds) {
2279
+ return api(
2280
+ cfg,
2281
+ `/me/executor-instances/${encodeURIComponent(id)}/refresh`,
2282
+ {
2283
+ method: "POST",
2284
+ body: JSON.stringify({ ttlSeconds })
2285
+ }
2286
+ );
2287
+ }
2288
+ async function registerInstance(cfg, state) {
2289
+ const subscription = await api(
2290
+ cfg,
2291
+ "/me/delivery-subscriptions/signalr",
2292
+ {
2293
+ method: "POST",
2294
+ body: JSON.stringify(executorSubscriptionInput(state.subscriptionName))
2295
+ }
2296
+ );
2297
+ return api(cfg, "/me/executor-instances", {
2298
+ method: "POST",
2299
+ body: JSON.stringify(executorRegistrationInput(state, subscription.id))
2300
+ });
2301
+ }
2302
+ async function ensureExecutorInstance(cfg, located) {
2303
+ const { state, path } = located;
2304
+ state.laneId ??= state.instanceKey;
2305
+ const data = await registerInstance(cfg, state);
2306
+ state.instanceId = data.id;
2307
+ state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
2308
+ writeFileSync6(path, JSON.stringify(state, null, 2) + "\n");
2309
+ return data;
2310
+ }
2311
+ function readExecutorState(start = process.cwd()) {
2312
+ const semPath = resolveSemPathForRead(start);
2313
+ const sem = semPath ? readSem(semPath) : void 0;
2314
+ const path = join8(
2315
+ sem ? dirname5(sem.path) : join8(start, ".sechroom"),
2316
+ EXECUTOR_STATE
2317
+ );
2318
+ if (!existsSync7(path)) return void 0;
2319
+ return {
2320
+ state: JSON.parse(readFileSync5(path, "utf8")),
2321
+ path
2322
+ };
1923
2323
  }
1924
- async function ensureSubscription(cfg, name, filter) {
2324
+ async function drainStdin() {
2325
+ if (process.stdin.isTTY) return;
2326
+ for await (const _chunk of process.stdin) {
2327
+ }
2328
+ }
2329
+ async function api(cfg, path, init) {
1925
2330
  const token = await requireToken(cfg);
1926
- const resp = await fetch(`${cfg.baseUrl}/me/delivery-subscriptions/signalr`, {
1927
- method: "POST",
2331
+ const response = await fetch(`${cfg.baseUrl}${path}`, {
2332
+ ...init,
1928
2333
  headers: {
1929
2334
  authorization: `Bearer ${token}`,
1930
2335
  tenant: cfg.tenant,
1931
2336
  "content-type": "application/json",
1932
2337
  "x-sechroom-surface": "cli"
1933
- },
1934
- body: JSON.stringify({ name, enabled: true, filter })
2338
+ }
1935
2339
  });
1936
- if (!resp.ok)
2340
+ if (!response.ok)
1937
2341
  fail(
1938
- `Could not register the SignalR subscription (HTTP ${resp.status}): ${await resp.text()}`
2342
+ `${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
1939
2343
  );
1940
- return await resp.json();
2344
+ return response.json();
1941
2345
  }
1942
- async function openConnection(cfg, onEvent, executorInstanceId) {
1943
- const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
1944
- const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
1945
- transport: HttpTransportType.LongPolling,
1946
- accessTokenFactory: () => requireToken(cfg)
1947
- }).withAutomaticReconnect().build();
1948
- conn.on("ReceiveMessage", onEvent);
1949
- conn.onreconnecting(
1950
- () => process.stderr.write(style.dim("channel: reconnecting\u2026\n"))
1951
- );
1952
- conn.onclose((e) => {
1953
- if (e) process.stderr.write(err(`channel closed: ${e.message}
1954
- `));
1955
- });
1956
- await conn.start();
1957
- return conn;
2346
+ function parseInteger(value) {
2347
+ const parsed = Number.parseInt(value, 10);
2348
+ if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
2349
+ return parsed;
1958
2350
  }
1959
- function holdOpen(conn) {
1960
- return new Promise((resolve3) => {
2351
+ function holdHeartbeat(tick, intervalMs) {
2352
+ return new Promise((resolve3, reject) => {
2353
+ const timer = setInterval(() => void tick().catch(reject), intervalMs);
1961
2354
  const stop = () => {
1962
- void conn.stop().finally(resolve3);
2355
+ clearInterval(timer);
2356
+ resolve3();
1963
2357
  };
1964
- process.on("SIGINT", stop);
1965
- process.on("SIGTERM", stop);
2358
+ process.once("SIGINT", stop);
2359
+ process.once("SIGTERM", stop);
1966
2360
  });
1967
2361
  }
1968
- function parseEvent(payload) {
1969
- let data = payload;
1970
- if (typeof payload === "string") {
1971
- try {
1972
- data = JSON.parse(payload);
1973
- } catch {
1974
- return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
1975
- }
1976
- }
1977
- const obj = data ?? {};
1978
- const inner = obj.data ?? obj;
1979
- const rawTags = inner.tags ?? inner.Tags;
1980
- return {
1981
- eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
1982
- memoryId: str(inner.memoryId ?? inner.MemoryId),
1983
- workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
1984
- tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
1985
- };
1986
- }
1987
- function shouldDeliver(payload, filter) {
1988
- const { workspaceId, tags } = parseEvent(payload);
1989
- if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
1990
- return false;
1991
- if (filter.tags.length > 0) {
1992
- if (!tags) return false;
1993
- return facetedTagMatch(tags, filter.tags);
1994
- }
1995
- return true;
1996
- }
1997
- function makeDeliver(filter, seen, forward) {
1998
- return (payload) => {
1999
- if (!shouldDeliver(payload, filter)) return;
2000
- const { memoryId } = parseEvent(payload);
2001
- if (memoryId) {
2002
- if (seen.has(memoryId)) return;
2003
- seen.add(memoryId);
2004
- }
2005
- forward(payload);
2006
- };
2007
- }
2008
- async function reconcile(cfg, filter, deliver) {
2009
- if (filter.workspaceScope.length === 0) {
2010
- process.stderr.write(
2011
- style.dim(
2012
- "channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
2013
- )
2362
+
2363
+ // src/commands/channel.ts
2364
+ function registerChannel(program2) {
2365
+ const channel = program2.command("channel").description(
2366
+ "Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
2367
+ );
2368
+ const withFilterOpts = (c) => c.option(
2369
+ "--name <name>",
2370
+ "Deprecated: ignored; the installed executor selects its delivery subscription"
2371
+ ).option(
2372
+ "--tag <tag...>",
2373
+ "Deprecated: executor eligibility comes from the installed capability advertisement"
2374
+ ).option(
2375
+ "--workspace <wsp...>",
2376
+ "Deprecated: workspace authority is resolved by the server"
2377
+ ).option(
2378
+ "--executor-instance <id>",
2379
+ "Deprecated: the instance is read from .sechroom/executor.json"
2380
+ );
2381
+ withFilterOpts(
2382
+ channel.command("connect").description(
2383
+ "Register a SignalR subscription and stream matched events to stdout"
2384
+ )
2385
+ ).action(async (opts, cmd) => {
2386
+ const json = Boolean(cmd.optsWithGlobals().json);
2387
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2388
+ warnLegacyChannelOptions(opts);
2389
+ const located = requireExecutorState();
2390
+ const instance = await ensureExecutorInstance(cfg, located);
2391
+ const deliver = (payload) => process.stdout.write(
2392
+ (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
2014
2393
  );
2015
- return;
2016
- }
2017
- if (filter.tags.length === 0) return;
2018
- let token;
2019
- try {
2020
- token = await requireToken(cfg);
2021
- } catch {
2022
- return;
2023
- }
2024
- const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
2025
- let recovered = 0;
2026
- for (const ws of filter.workspaceScope) {
2027
- try {
2028
- const resp = await fetch(
2029
- `${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
2394
+ const drain = createClaimDrain(cfg, instance.id, deliver);
2395
+ const conn = await openConnection(
2396
+ cfg,
2397
+ () => {
2398
+ void drain().catch(
2399
+ (e) => process.stderr.write(err(`channel claim failed: ${String(e)}
2400
+ `))
2401
+ );
2402
+ },
2403
+ instance.id
2404
+ );
2405
+ await drain();
2406
+ const stopReconciliation = startOfferReconciliation(drain);
2407
+ const stopHeartbeat = startExecutorHeartbeat(
2408
+ () => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
2409
+ located.state.refreshAfterSeconds * 1e3
2410
+ );
2411
+ if (json) {
2412
+ emit(
2030
2413
  {
2031
- headers: {
2032
- authorization: `Bearer ${token}`,
2033
- tenant: cfg.tenant,
2034
- "x-sechroom-surface": "cli"
2035
- }
2036
- }
2414
+ connected: true,
2415
+ tenant: cfg.tenant,
2416
+ executorInstanceId: instance.id,
2417
+ instanceKey: located.state.instanceKey,
2418
+ laneId: located.state.laneId
2419
+ },
2420
+ true
2037
2421
  );
2038
- if (!resp.ok) {
2039
- process.stderr.write(
2040
- err(
2041
- `channel: reconcile query for ${ws} failed (HTTP ${resp.status})
2042
- `
2043
- )
2044
- );
2045
- continue;
2046
- }
2047
- const data = await resp.json();
2048
- for (const m of data.results ?? []) {
2049
- if (!m.id) continue;
2050
- deliver({
2051
- eventType: "reconcile",
2052
- memoryId: m.id,
2053
- workspaceId: ws,
2054
- tags: m.tags ?? []
2055
- });
2056
- recovered++;
2057
- }
2058
- } catch (e) {
2422
+ } else {
2059
2423
  process.stderr.write(
2060
- err(`channel: reconcile error for ${ws}: ${String(e)}
2061
- `)
2424
+ style.green("channel connected") + style.dim(
2425
+ ` \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
2426
+ `
2427
+ ) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
2062
2428
  );
2063
2429
  }
2064
- }
2065
- if (recovered > 0)
2430
+ try {
2431
+ await holdOpen(conn);
2432
+ } finally {
2433
+ stopReconciliation();
2434
+ stopHeartbeat();
2435
+ }
2436
+ });
2437
+ withFilterOpts(
2438
+ channel.command("mcp").description(
2439
+ "Run as a Claude Code channel (local-stdio MCP server) \u2014 push matched events into the session"
2440
+ )
2441
+ ).action(async (opts, cmd) => {
2442
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2443
+ warnLegacyChannelOptions(opts);
2444
+ const located = requireExecutorState();
2445
+ const instance = await ensureExecutorInstance(cfg, located);
2446
+ const mcp = new Server(
2447
+ { name: "sechroom", version: "0.1.0" },
2448
+ {
2449
+ capabilities: { experimental: { "claude/channel": {} } },
2450
+ instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch delivered here has already been atomically claimed for this executor. Load the memory id from the event and retain the lease and claim token for holder-bound completion.'
2451
+ }
2452
+ );
2453
+ await mcp.connect(new StdioServerTransport());
2454
+ const deliver = (payload) => {
2455
+ const { content, meta } = summarizeEvent(payload);
2456
+ void mcp.notification({
2457
+ method: "notifications/claude/channel",
2458
+ params: { content, meta }
2459
+ }).catch(
2460
+ (e) => process.stderr.write(err(`channel push failed: ${String(e)}
2461
+ `))
2462
+ );
2463
+ };
2464
+ const drain = createClaimDrain(cfg, instance.id, deliver);
2465
+ const conn = await openConnection(
2466
+ cfg,
2467
+ () => {
2468
+ void drain().catch(
2469
+ (e) => process.stderr.write(err(`channel claim failed: ${String(e)}
2470
+ `))
2471
+ );
2472
+ },
2473
+ instance.id
2474
+ );
2475
+ await drain();
2476
+ const stopReconciliation = startOfferReconciliation(drain);
2477
+ const stopHeartbeat = startExecutorHeartbeat(
2478
+ () => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
2479
+ located.state.refreshAfterSeconds * 1e3
2480
+ );
2066
2481
  process.stderr.write(
2067
2482
  style.dim(
2068
- `channel: reconciled ${recovered} already-queued event(s) on connect.
2483
+ `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
2069
2484
  `
2070
2485
  )
2071
2486
  );
2072
- }
2073
- function facetedTagMatch(eventTags, filterTags) {
2074
- const have = new Set(eventTags);
2075
- const groups = /* @__PURE__ */ new Map();
2076
- for (const f of filterTags) {
2077
- const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
2078
- const group = groups.get(ns) ?? [];
2079
- group.push(f);
2080
- groups.set(ns, group);
2081
- }
2082
- for (const [ns, group] of groups) {
2083
- const ok2 = group.some(
2084
- (f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
2085
- );
2086
- if (!ok2) return false;
2087
- }
2088
- return true;
2089
- }
2090
- function namespaceOf(tag) {
2091
- const i = tag.indexOf(":");
2092
- return i >= 0 ? tag.slice(0, i) : tag;
2093
- }
2094
- function summarizeEvent(payload) {
2095
- const { eventType, memoryId, workspaceId } = parseEvent(payload);
2096
- const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
2097
- const meta = {};
2098
- if (eventType) meta.event_type = eventType;
2099
- if (memoryId) meta.memory_id = memoryId;
2100
- if (workspaceId) meta.workspace_id = workspaceId;
2101
- return { content, meta };
2102
- }
2103
- function str(v) {
2104
- return typeof v === "string" ? v : v == null ? "" : String(v);
2105
- }
2106
-
2107
- // src/commands/chat.ts
2108
- function registerChat(program2) {
2109
- const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
2110
- chat.addHelpText(
2111
- "after",
2112
- `
2113
- Examples:
2114
- $ sechroom chat send C0123456789 "deploy is green" --surface slack
2115
- $ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
2116
- $ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
2117
- $ sechroom chat messages --surface slack
2118
- $ sechroom chat replies 1718049600.123456 --surface slack
2119
- $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
2120
- );
2121
- 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) => {
2122
- const { surface, ...globals } = cmd.optsWithGlobals();
2123
- const json = Boolean(cmd.optsWithGlobals().json);
2124
- const cfg = resolveConfig(globals);
2125
- const data = await runApi("Sending message", async () => {
2126
- const client = await makeClient(cfg);
2127
- return client.POST("/chat/channel-messages/{surface}", {
2128
- params: { path: { surface: String(surface) } },
2129
- body: {
2130
- channelId,
2131
- text: text2,
2132
- guildId: opts.guild ?? null,
2133
- attachedMemoryId: opts.memory ?? null,
2134
- trackReplies: opts.track,
2135
- parentMessage: opts.parent ?? null,
2136
- source: opts.source,
2137
- as: opts.as
2138
- }
2139
- });
2140
- });
2141
- if (!data.ok) {
2142
- if (json) {
2143
- emit(data, true);
2144
- } else {
2145
- process.stderr.write(
2146
- `${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
2487
+ try {
2488
+ await holdOpen(conn);
2489
+ } finally {
2490
+ stopReconciliation();
2491
+ stopHeartbeat();
2492
+ }
2493
+ });
2494
+ channel.command("install").description(
2495
+ "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
2496
+ ).option(
2497
+ "--workspace <wsp...>",
2498
+ "Deprecated: accepted only to migrate an existing managed entry"
2499
+ ).option(
2500
+ "--tag <tag...>",
2501
+ "Deprecated: accepted only to migrate an existing managed entry"
2502
+ ).option(
2503
+ "--name <name>",
2504
+ "MCP server + subscription name (idempotent per name)",
2505
+ "sechroom-channel"
2506
+ ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
2507
+ const path = join9(process.cwd(), ".mcp.json");
2508
+ const dryRun = Boolean(opts.dryRun);
2509
+ const args = ["channel", "mcp"];
2510
+ const entry = { command: "sechroom", args };
2511
+ const config2 = readMcpConfig(path);
2512
+ config2.mcpServers ??= {};
2513
+ const existing = config2.mcpServers[opts.name];
2514
+ const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
2515
+ if (status !== "current" && !dryRun) {
2516
+ config2.mcpServers[opts.name] = entry;
2517
+ mkdirSync7(dirname6(path), { recursive: true });
2518
+ writeFileSync7(path, JSON.stringify(config2, null, 2) + "\n");
2519
+ }
2520
+ const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
2521
+ process.stdout.write(`${style.green("channel")} ${path} (${verb})
2522
+ `);
2523
+ process.stdout.write(
2524
+ style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
2525
+ `)
2526
+ );
2527
+ if (status !== "current") {
2528
+ process.stdout.write(
2529
+ style.dim(
2530
+ `
2531
+ Load it (Channels research preview) by launching your agent with:
2532
+ claude --dangerously-load-development-channels server:${opts.name}
2147
2533
  `
2148
- );
2149
- }
2150
- process.exit(1);
2534
+ )
2535
+ );
2151
2536
  }
2152
- const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
2153
- emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
2154
- });
2155
- chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
2156
- const { surface, ...globals } = cmd.optsWithGlobals();
2157
- const cfg = resolveConfig(globals);
2158
- const data = await runApi("Fetching messages", async () => {
2159
- const client = await makeClient(cfg);
2160
- return client.GET("/chat/channel-messages/{surface}", {
2161
- params: { path: { surface: String(surface) } }
2162
- });
2163
- });
2164
- emit(data, cmd.optsWithGlobals().json);
2165
- });
2166
- chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
2167
- const cfg = resolveConfig(cmd.optsWithGlobals());
2168
- const data = await runApi("Fetching replies", async () => {
2169
- const client = await makeClient(cfg);
2170
- return client.GET("/chat/channel-messages/by-id/{id}/replies", {
2171
- params: { path: { id: messageId } }
2172
- });
2173
- });
2174
- emit(data, cmd.optsWithGlobals().json);
2175
- });
2176
- chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
2177
- const cfg = resolveConfig(cmd.optsWithGlobals());
2178
- const data = await runApi("Stopping reply tracking", async () => {
2179
- const client = await makeClient(cfg);
2180
- return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
2181
- params: { path: { id: messageId } },
2182
- body: {}
2183
- });
2184
- });
2185
- emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
2537
+ warnIfSechroomNotOnPath();
2538
+ if ((opts.workspace?.length ?? 0) > 0 || (opts.tag?.length ?? 0) > 0)
2539
+ process.stderr.write(
2540
+ style.dim(
2541
+ "channel: --workspace/--tag are retired; the managed entry now uses the installed executor advertisement.\n"
2542
+ )
2543
+ );
2186
2544
  });
2187
- }
2188
-
2189
- // src/commands/checkpoint.ts
2190
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
2191
- import { dirname as dirname7, join as join10 } from "path";
2192
-
2193
- // src/commands/hook.ts
2194
- import { createHash as createHash2 } from "crypto";
2195
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
2196
- import { dirname as dirname6, join as join9 } from "path";
2545
+ channel.addHelpText(
2546
+ "after",
2547
+ `
2548
+ Examples:
2549
+ $ sechroom executor install configure capability + lane advertisement
2550
+ $ sechroom channel connect claim WLP dispatches and stream them to stdout
2197
2551
 
2198
- // src/sem.ts
2199
- import { dirname as dirname5, join as join8 } from "path";
2200
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
2201
- var SEM_FILE = join8(".sechroom", "lane.json");
2202
- var STATE_DIR_NAME2 = ".sechroom";
2203
- function localSemPath(cwd = process.cwd()) {
2204
- return join8(cwd, SEM_FILE);
2205
- }
2206
- function resolveSemPathForRead(start = process.cwd()) {
2207
- let dir = start;
2208
- while (true) {
2209
- const candidate = join8(dir, SEM_FILE);
2210
- if (existsSync7(candidate)) return candidate;
2211
- const parent = dirname5(dir);
2212
- if (parent === dir) return void 0;
2213
- dir = parent;
2214
- }
2215
- }
2216
- function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
2217
- try {
2218
- let dir = start;
2219
- let gitPath;
2220
- for (; ; ) {
2221
- const candidate = join8(dir, ".git");
2222
- if (existsSync7(candidate)) {
2223
- gitPath = candidate;
2224
- break;
2225
- }
2226
- const parent = dirname5(dir);
2227
- if (parent === dir) break;
2228
- dir = parent;
2229
- }
2230
- if (!gitPath || statSync(gitPath).isDirectory()) return lane;
2231
- const gitFile = readFileSync5(gitPath, "utf8");
2232
- const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
2233
- if (!common) return lane;
2234
- const worktreesDir = join8(common[1], "worktrees");
2235
- const siblings = readdirSync(worktreesDir).filter((n) => {
2236
- try {
2237
- return statSync(join8(worktreesDir, n)).isDirectory();
2238
- } catch {
2239
- return false;
2240
- }
2241
- });
2242
- return laneWithWorktreeSuffix(lane, gitFile, siblings);
2243
- } catch {
2244
- return lane;
2245
- }
2246
- }
2247
- function laneWithWorktreeSuffix(lane, gitFile, siblings) {
2248
- const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
2249
- if (!m) return lane;
2250
- const idx = [...siblings].sort().indexOf(m[1]);
2251
- return idx < 0 ? lane : `${lane}-${idx + 2}`;
2552
+ # Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
2553
+ $ sechroom channel install migrate/install the exact-instance channel
2554
+ # then: claude --dangerously-load-development-channels server:sechroom-channel`
2555
+ );
2252
2556
  }
2253
- function serializeSem(values) {
2254
- return JSON.stringify(values, null, 2) + "\n";
2557
+ function requireExecutorState() {
2558
+ const located = readExecutorState();
2559
+ if (!located)
2560
+ return fail(
2561
+ "channel requires an installed executor advertisement; run `sechroom executor install` first."
2562
+ );
2563
+ return located;
2255
2564
  }
2256
- function readSem(path) {
2257
- const p = path ?? resolveSemPathForRead();
2258
- if (!p || !existsSync7(p)) return void 0;
2259
- return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
2565
+ function warnLegacyChannelOptions(opts) {
2566
+ if (!opts.name && (opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
2567
+ return;
2568
+ process.stderr.write(
2569
+ style.dim(
2570
+ "channel: --name, --workspace, --tag, and --executor-instance are retired; delivery, eligibility, and identity come from the installed executor advertisement.\n"
2571
+ )
2572
+ );
2260
2573
  }
2261
- function readLocalSemValues(cwd = process.cwd()) {
2262
- const next = join8(cwd, SEM_FILE);
2263
- if (existsSync7(next)) return readSem(next)?.values ?? {};
2264
- return {};
2574
+ function createClaimDrain(cfg, executorInstanceId, deliver, dependencies = {}) {
2575
+ let active2;
2576
+ const state = {};
2577
+ return () => {
2578
+ active2 ??= drainClaims(cfg, executorInstanceId, deliver, {
2579
+ ...dependencies,
2580
+ state
2581
+ }).finally(() => {
2582
+ active2 = void 0;
2583
+ });
2584
+ return active2;
2585
+ };
2265
2586
  }
2266
- function parseLaneJson(text2) {
2267
- try {
2268
- const parsed = JSON.parse(text2);
2269
- const out = {};
2270
- for (const [k, v] of Object.entries(parsed)) {
2271
- if (typeof v === "string") out[k] = v;
2587
+ function startOfferReconciliation(drain, intervalMilliseconds = 5e3, dependencies = {}) {
2588
+ const schedule = dependencies.setInterval ?? setInterval;
2589
+ const cancel = dependencies.clearInterval ?? clearInterval;
2590
+ const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel claim failed: ${String(error)}
2591
+ `)));
2592
+ const timer = schedule(() => {
2593
+ void drain().catch(onError);
2594
+ }, intervalMilliseconds);
2595
+ return () => cancel(timer);
2596
+ }
2597
+ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}) {
2598
+ const schedule = dependencies.setInterval ?? setInterval;
2599
+ const cancel = dependencies.clearInterval ?? clearInterval;
2600
+ const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel heartbeat failed: ${String(error)}
2601
+ `)));
2602
+ const timer = schedule(() => {
2603
+ void refresh().catch(onError);
2604
+ }, intervalMilliseconds);
2605
+ return () => cancel(timer);
2606
+ }
2607
+ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
2608
+ const request = dependencies.request ?? api;
2609
+ const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
2610
+ const idempotencyKey = dependencies.idempotencyKey ?? ((offer) => `channel:${offer.generationId}`);
2611
+ const state = dependencies.state ?? {};
2612
+ for (; ; ) {
2613
+ if (state.pendingIdempotencyKey) {
2614
+ const replay = await request(
2615
+ cfg,
2616
+ `/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
2617
+ {
2618
+ method: "POST",
2619
+ body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
2620
+ }
2621
+ );
2622
+ state.pendingIdempotencyKey = void 0;
2623
+ if (replay.outcome === "Claimed" || replay.outcome === "AlreadyHeld") {
2624
+ deliver(replay);
2625
+ continue;
2626
+ }
2627
+ return;
2272
2628
  }
2273
- return out;
2274
- } catch {
2275
- return {};
2629
+ const offers = await request(
2630
+ cfg,
2631
+ `/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers`
2632
+ );
2633
+ const offer = offers[0];
2634
+ if (!offer) return;
2635
+ if (offer.suggestedClaimDelayMs > 0)
2636
+ await sleep(offer.suggestedClaimDelayMs);
2637
+ state.pendingIdempotencyKey = idempotencyKey(offer);
2638
+ const claim = await request(
2639
+ cfg,
2640
+ `/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
2641
+ {
2642
+ method: "POST",
2643
+ body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
2644
+ }
2645
+ );
2646
+ state.pendingIdempotencyKey = void 0;
2647
+ if (claim.outcome === "NoOffer") return;
2648
+ if (claim.outcome === "Claimed" || claim.outcome === "AlreadyHeld")
2649
+ deliver(claim);
2650
+ else return;
2276
2651
  }
2277
2652
  }
2278
- var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
2279
- function writeSem(values, path = localSemPath()) {
2280
- mkdirSync6(dirname5(path), { recursive: true });
2281
- writeFileSync6(path, serializeSem(values));
2282
- ensureSemIgnored(path);
2283
- ensureContinuityScaffold(path);
2284
- return path;
2285
- }
2286
- function ensureStateDirIgnored(cwd = process.cwd()) {
2287
- ensureSemIgnored(localSemPath(cwd));
2288
- }
2289
- var CONTINUITY_FILE_NAME = "continuity.json";
2290
- var CONTINUITY_SCAFFOLD = JSON.stringify(
2291
- {
2292
- _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.",
2293
- objective: "",
2294
- state: "",
2295
- lastAction: "",
2296
- nextAction: "",
2297
- resumeInstruction: "",
2298
- constraints: [],
2299
- questions: [],
2300
- artifacts: [],
2301
- confidence: null
2302
- },
2303
- null,
2304
- 2
2305
- ) + "\n";
2306
- function ensureContinuityScaffold(semPath) {
2653
+ function readMcpConfig(path) {
2654
+ if (!existsSync8(path)) return {};
2655
+ const raw = readFileSync6(path, "utf8");
2656
+ if (!raw.trim()) return {};
2307
2657
  try {
2308
- const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
2309
- if (existsSync7(target)) return;
2310
- writeFileSync6(target, CONTINUITY_SCAFFOLD);
2658
+ return JSON.parse(raw);
2311
2659
  } catch {
2660
+ return fail(
2661
+ `Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
2662
+ );
2312
2663
  }
2313
2664
  }
2314
- function ignoresSem(content) {
2315
- return content.split("\n").some((line) => {
2316
- const t = line.trim();
2317
- 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}`;
2665
+ async function openConnection(cfg, onEvent, executorInstanceId) {
2666
+ const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
2667
+ const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
2668
+ transport: HttpTransportType.LongPolling,
2669
+ accessTokenFactory: () => requireToken(cfg)
2670
+ }).withAutomaticReconnect().build();
2671
+ conn.on("ReceiveMessage", onEvent);
2672
+ conn.onreconnecting(
2673
+ () => process.stderr.write(style.dim("channel: reconnecting\u2026\n"))
2674
+ );
2675
+ conn.onclose((e) => {
2676
+ if (e) process.stderr.write(err(`channel closed: ${e.message}
2677
+ `));
2318
2678
  });
2679
+ await conn.start();
2680
+ return conn;
2319
2681
  }
2320
- function inGitRepo(startDir) {
2321
- let dir = startDir;
2322
- for (; ; ) {
2323
- if (existsSync7(join8(dir, ".git"))) return true;
2324
- const parent = dirname5(dir);
2325
- if (parent === dir) return false;
2326
- dir = parent;
2327
- }
2682
+ function holdOpen(conn) {
2683
+ return new Promise((resolve3) => {
2684
+ const stop = () => {
2685
+ void conn.stop().finally(resolve3);
2686
+ };
2687
+ process.on("SIGINT", stop);
2688
+ process.on("SIGTERM", stop);
2689
+ });
2328
2690
  }
2329
- function resolveGitignoreTarget(startDir) {
2330
- let dir = startDir;
2331
- for (; ; ) {
2332
- const gi = join8(dir, ".gitignore");
2333
- if (existsSync7(gi)) return { path: gi, exists: true };
2334
- const parent = dirname5(dir);
2335
- if (existsSync7(join8(dir, ".git")) || parent === dir) {
2336
- return { path: join8(startDir, ".gitignore"), exists: false };
2691
+ function parseEvent(payload) {
2692
+ let data = payload;
2693
+ if (typeof payload === "string") {
2694
+ try {
2695
+ data = JSON.parse(payload);
2696
+ } catch {
2697
+ return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
2337
2698
  }
2338
- dir = parent;
2339
2699
  }
2700
+ const obj = data ?? {};
2701
+ const envelope = obj.data ?? obj;
2702
+ const inner = envelope.offer ?? envelope;
2703
+ const rawTags = inner.tags ?? inner.Tags;
2704
+ return {
2705
+ eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
2706
+ memoryId: str(inner.memoryId ?? inner.MemoryId),
2707
+ workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
2708
+ tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
2709
+ };
2340
2710
  }
2341
- function ensureSemIgnored(semPath) {
2342
- try {
2343
- const checkoutDir = dirname5(dirname5(semPath));
2344
- if (!inGitRepo(checkoutDir)) return;
2345
- const target = resolveGitignoreTarget(checkoutDir);
2346
- if (target.exists) {
2347
- const content = readFileSync5(target.path, "utf8");
2348
- if (ignoresSem(content)) return;
2349
- const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
2350
- appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
2351
- `);
2352
- } else {
2353
- writeFileSync6(target.path, `${STATE_DIR_IGNORE}
2354
- `);
2711
+ function summarizeEvent(payload) {
2712
+ const { eventType, memoryId, workspaceId } = parseEvent(payload);
2713
+ const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
2714
+ const meta = {};
2715
+ if (eventType) meta.event_type = eventType;
2716
+ if (memoryId) meta.memory_id = memoryId;
2717
+ if (workspaceId) meta.workspace_id = workspaceId;
2718
+ const claim = payload ?? {};
2719
+ if (claim.outcome) meta.claim_outcome = claim.outcome;
2720
+ if (claim.lease?.id) meta.lease_id = claim.lease.id;
2721
+ if (claim.claimToken) meta.claim_token = claim.claimToken;
2722
+ return { content, meta };
2723
+ }
2724
+ function str(v) {
2725
+ return typeof v === "string" ? v : v == null ? "" : String(v);
2726
+ }
2727
+
2728
+ // src/commands/chat.ts
2729
+ function registerChat(program2) {
2730
+ const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
2731
+ chat.addHelpText(
2732
+ "after",
2733
+ `
2734
+ Examples:
2735
+ $ sechroom chat send C0123456789 "deploy is green" --surface slack
2736
+ $ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
2737
+ $ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
2738
+ $ sechroom chat messages --surface slack
2739
+ $ sechroom chat replies 1718049600.123456 --surface slack
2740
+ $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
2741
+ );
2742
+ 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) => {
2743
+ const { surface, ...globals } = cmd.optsWithGlobals();
2744
+ const json = Boolean(cmd.optsWithGlobals().json);
2745
+ const cfg = resolveConfig(globals);
2746
+ const data = await runApi("Sending message", async () => {
2747
+ const client = await makeClient(cfg);
2748
+ return client.POST("/chat/channel-messages/{surface}", {
2749
+ params: { path: { surface: String(surface) } },
2750
+ body: {
2751
+ channelId,
2752
+ text: text2,
2753
+ guildId: opts.guild ?? null,
2754
+ attachedMemoryId: opts.memory ?? null,
2755
+ trackReplies: opts.track,
2756
+ parentMessage: opts.parent ?? null,
2757
+ source: opts.source,
2758
+ as: opts.as
2759
+ }
2760
+ });
2761
+ });
2762
+ if (!data.ok) {
2763
+ if (json) {
2764
+ emit(data, true);
2765
+ } else {
2766
+ process.stderr.write(
2767
+ `${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
2768
+ `
2769
+ );
2770
+ }
2771
+ process.exit(1);
2355
2772
  }
2356
- } catch {
2357
- }
2773
+ const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
2774
+ emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
2775
+ });
2776
+ chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
2777
+ const { surface, ...globals } = cmd.optsWithGlobals();
2778
+ const cfg = resolveConfig(globals);
2779
+ const data = await runApi("Fetching messages", async () => {
2780
+ const client = await makeClient(cfg);
2781
+ return client.GET("/chat/channel-messages/{surface}", {
2782
+ params: { path: { surface: String(surface) } }
2783
+ });
2784
+ });
2785
+ emit(data, cmd.optsWithGlobals().json);
2786
+ });
2787
+ chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
2788
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2789
+ const data = await runApi("Fetching replies", async () => {
2790
+ const client = await makeClient(cfg);
2791
+ return client.GET("/chat/channel-messages/by-id/{id}/replies", {
2792
+ params: { path: { id: messageId } }
2793
+ });
2794
+ });
2795
+ emit(data, cmd.optsWithGlobals().json);
2796
+ });
2797
+ chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
2798
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2799
+ const data = await runApi("Stopping reply tracking", async () => {
2800
+ const client = await makeClient(cfg);
2801
+ return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
2802
+ params: { path: { id: messageId } },
2803
+ body: {}
2804
+ });
2805
+ });
2806
+ emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
2807
+ });
2358
2808
  }
2359
2809
 
2810
+ // src/commands/checkpoint.ts
2811
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
2812
+ import { dirname as dirname8, join as join11 } from "path";
2813
+
2360
2814
  // src/commands/hook.ts
2815
+ import { createHash as createHash2 } from "crypto";
2816
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
2817
+ import { dirname as dirname7, join as join10 } from "path";
2361
2818
  async function readStdin() {
2362
2819
  if (process.stdin.isTTY) return "";
2363
2820
  const chunks = [];
@@ -2381,13 +2838,13 @@ function resolveLane(flagLane, cwd) {
2381
2838
  if (!base) return void 0;
2382
2839
  return applyWorktreeLaneSuffix(base, start);
2383
2840
  }
2384
- var INTENT_FILE = join9(".sechroom", "continuity.json");
2841
+ var INTENT_FILE = join10(".sechroom", "continuity.json");
2385
2842
  function resolveIntentPath(start) {
2386
2843
  let dir = start;
2387
2844
  for (; ; ) {
2388
- const candidate = join9(dir, INTENT_FILE);
2389
- if (existsSync8(candidate)) return candidate;
2390
- const parent = dirname6(dir);
2845
+ const candidate = join10(dir, INTENT_FILE);
2846
+ if (existsSync9(candidate)) return candidate;
2847
+ const parent = dirname7(dir);
2391
2848
  if (parent === dir) return void 0;
2392
2849
  dir = parent;
2393
2850
  }
@@ -2396,7 +2853,7 @@ function readIntent(start) {
2396
2853
  const path = resolveIntentPath(start);
2397
2854
  if (!path) return void 0;
2398
2855
  try {
2399
- return JSON.parse(readFileSync6(path, "utf8"));
2856
+ return JSON.parse(readFileSync7(path, "utf8"));
2400
2857
  } catch {
2401
2858
  return void 0;
2402
2859
  }
@@ -2438,14 +2895,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
2438
2895
  }
2439
2896
  function ledgerPath(start) {
2440
2897
  const intent = resolveIntentPath(start);
2441
- const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
2442
- return join9(dir, ".checkpoint-state.json");
2898
+ const dir = intent ? dirname7(intent) : join10(start, ".sechroom");
2899
+ return join10(dir, ".checkpoint-state.json");
2443
2900
  }
2444
2901
  function readLedger(start) {
2445
2902
  try {
2446
2903
  const p = ledgerPath(start);
2447
- if (!existsSync8(p)) return {};
2448
- return JSON.parse(readFileSync6(p, "utf8"));
2904
+ if (!existsSync9(p)) return {};
2905
+ return JSON.parse(readFileSync7(p, "utf8"));
2449
2906
  } catch {
2450
2907
  return {};
2451
2908
  }
@@ -2492,13 +2949,13 @@ function recordPush(start, intent) {
2492
2949
  } catch {
2493
2950
  mtimeMs = void 0;
2494
2951
  }
2495
- mkdirSync7(dirname6(p), { recursive: true });
2952
+ mkdirSync8(dirname7(p), { recursive: true });
2496
2953
  const ledger = {
2497
2954
  lastEpochMs: Date.now(),
2498
2955
  lastMtimeMs: mtimeMs,
2499
2956
  lastHash: intentHash(intent)
2500
2957
  };
2501
- writeFileSync7(p, JSON.stringify(ledger) + "\n");
2958
+ writeFileSync8(p, JSON.stringify(ledger) + "\n");
2502
2959
  } catch {
2503
2960
  }
2504
2961
  }
@@ -2751,10 +3208,10 @@ Examples:
2751
3208
  const client = await makeClient(cfg);
2752
3209
  return client.POST("/continuity/snapshots", { body });
2753
3210
  });
2754
- const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
3211
+ const path = resolveIntentPath(cwd) ?? join11(cwd, INTENT_FILE);
2755
3212
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2756
- mkdirSync8(dirname7(path), { recursive: true });
2757
- writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
3213
+ mkdirSync9(dirname8(path), { recursive: true });
3214
+ writeFileSync9(path, JSON.stringify(fileBody, null, 2) + "\n");
2758
3215
  recordPush(cwd, merged);
2759
3216
  if (json) {
2760
3217
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -2768,7 +3225,7 @@ Examples:
2768
3225
  }
2769
3226
 
2770
3227
  // src/commands/close.ts
2771
- import { readFileSync as readFileSync7 } from "fs";
3228
+ import { readFileSync as readFileSync8 } from "fs";
2772
3229
  var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
2773
3230
  function registerClose(program2) {
2774
3231
  program2.command("close").description(
@@ -2809,7 +3266,7 @@ Examples:
2809
3266
  );
2810
3267
  let bodyText;
2811
3268
  try {
2812
- bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
3269
+ bodyText = opts.file ? readFileSync8(opts.file, "utf8") : readFileSync8(0, "utf8");
2813
3270
  } catch {
2814
3271
  fail(
2815
3272
  opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
@@ -2977,561 +3434,227 @@ Examples:
2977
3434
  }
2978
3435
  });
2979
3436
  });
2980
- emitAction(`created snapshot ${style.bold(data.snapshotId)}`, data, cmd.optsWithGlobals().json);
2981
- });
2982
- continuity.command("snapshot-get <id>").description("Fetch a snapshot by id (GET /continuity/snapshots/{id})").action(async (id, _opts, cmd) => {
2983
- const cfg = resolveConfig(cmd.optsWithGlobals());
2984
- const data = await runApi("Fetching snapshot", async () => {
2985
- const client = await makeClient(cfg);
2986
- return client.GET("/continuity/snapshots/{id}", { params: { path: { id } } });
2987
- });
2988
- emit(data, cmd.optsWithGlobals().json);
2989
- });
2990
- continuity.command("snapshots").description("List the caller's own snapshots (GET /me/continuity/snapshots)").option("--scope <scope>", "Filter by scope").option("--lane <laneId>", "Filter by lane id").action(async (opts, cmd) => {
2991
- const cfg = resolveConfig(cmd.optsWithGlobals());
2992
- const data = await runApi("Listing snapshots", async () => {
2993
- const client = await makeClient(cfg);
2994
- return client.GET("/me/continuity/snapshots", {
2995
- params: {
2996
- query: {
2997
- ...opts.scope ? { scope: opts.scope } : {},
2998
- ...opts.lane ? { laneId: opts.lane } : {}
2999
- }
3000
- }
3001
- });
3002
- });
3003
- emit(data, cmd.optsWithGlobals().json);
3004
- });
3005
- continuity.command("resume-me").description("Resume the caller's own lane (POST /continuity/resume/me)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the bundle").option("--changed-since <iso>", "Only include changes since this ISO timestamp").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3006
- const cfg = resolveConfig(cmd.optsWithGlobals());
3007
- const data = await runApi("Resuming", async () => {
3008
- const client = await makeClient(cfg);
3009
- return client.POST("/continuity/resume/me", {
3010
- body: {
3011
- workspaceId: opts.workspace ?? null,
3012
- maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3013
- includeLookingAtMyself: opts.lookingAtMyself ? true : null,
3014
- changedSince: opts.changedSince ?? null
3015
- }
3016
- });
3017
- });
3018
- emit(data, cmd.optsWithGlobals().json);
3019
- });
3020
- continuity.command("resume-lane <laneId>").description("Resume a specific lane (POST /continuity/resume/lane)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the bundle").option("--changed-since <iso>", "Only include changes since this ISO timestamp").option("--looking-at-myself", "Include the caller's own changes", false).action(async (laneId, opts, cmd) => {
3021
- const cfg = resolveConfig(cmd.optsWithGlobals());
3022
- const data = await runApi("Resuming lane", async () => {
3023
- const client = await makeClient(cfg);
3024
- return client.POST("/continuity/resume/lane", {
3025
- body: {
3026
- laneId,
3027
- workspaceId: opts.workspace ?? null,
3028
- maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3029
- includeLookingAtMyself: opts.lookingAtMyself ? true : null,
3030
- changedSince: opts.changedSince ?? null
3031
- }
3032
- });
3033
- });
3034
- emit(data, cmd.optsWithGlobals().json);
3035
- });
3036
- continuity.command("changed-since").description("What changed since a timestamp (POST /continuity/changed-since)").requiredOption("--since <iso>", "ISO-8601 timestamp to compare against").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3037
- const cfg = resolveConfig(cmd.optsWithGlobals());
3038
- const data = await runApi("Computing changes", async () => {
3039
- const client = await makeClient(cfg);
3040
- return client.POST("/continuity/changed-since", {
3041
- body: {
3042
- since: opts.since,
3043
- includeLookingAtMyself: opts.lookingAtMyself ? true : null
3044
- }
3045
- });
3046
- });
3047
- emit(data, cmd.optsWithGlobals().json);
3048
- });
3049
- continuity.command("load-set").description("Derive the active load set (POST /continuity/load-set/derive)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the load set").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3050
- const cfg = resolveConfig(cmd.optsWithGlobals());
3051
- const data = await runApi("Deriving load set", async () => {
3052
- const client = await makeClient(cfg);
3053
- return client.POST("/continuity/load-set/derive", {
3054
- body: {
3055
- workspaceId: opts.workspace ?? null,
3056
- maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3057
- includeLookingAtMyself: opts.lookingAtMyself ? true : null
3058
- }
3059
- });
3060
- });
3061
- emit(data, cmd.optsWithGlobals().json);
3062
- });
3063
- continuity.command("grant <snapshotId>").description("Grant another operator read access (POST /continuity/snapshots/{snapshotId}/grants)").requiredOption("--grantee <userId>", "Sechroom user id being granted read access").option("--source <source>", "Permission-set source kind", "TenantRole").option("--source-id <sourceId>", "Permission-set source id (e.g. a tenant role)", "viewer").option("--valid-from <iso>", "Optional ISO-8601 grant start").option("--valid-to <iso>", "Optional ISO-8601 grant expiry").action(async (snapshotId, opts, cmd) => {
3064
- const cfg = resolveConfig(cmd.optsWithGlobals());
3065
- const data = await runApi("Minting grant", async () => {
3066
- const client = await makeClient(cfg);
3067
- return client.POST("/continuity/snapshots/{snapshotId}/grants", {
3068
- params: { path: { snapshotId } },
3069
- body: {
3070
- userId: opts.grantee,
3071
- kind: "Allow",
3072
- source: opts.source,
3073
- sourceId: opts.sourceId,
3074
- ...opts.validFrom ? { validFrom: opts.validFrom } : {},
3075
- ...opts.validTo ? { validTo: opts.validTo } : {}
3076
- }
3077
- });
3078
- });
3079
- emitAction(
3080
- `granted ${style.bold(data.userId)} read on ${style.bold(snapshotId)} ${style.dim(`(grant ${data.grantId})`)}`,
3081
- data,
3082
- cmd.optsWithGlobals().json
3083
- );
3084
- });
3085
- continuity.command("revoke-grant <snapshotId> <grantId>").description("Revoke a grant (DELETE /continuity/snapshots/{snapshotId}/grants/{grantId})").action(async (snapshotId, grantId, _opts, cmd) => {
3086
- const cfg = resolveConfig(cmd.optsWithGlobals());
3087
- const data = await runApi("Revoking grant", async () => {
3088
- const client = await makeClient(cfg);
3089
- return client.DELETE("/continuity/snapshots/{snapshotId}/grants/{grantId}", {
3090
- params: { path: { snapshotId, grantId } },
3091
- body: {}
3092
- });
3093
- });
3094
- emitAction(
3095
- `revoked grant ${style.bold(grantId)} on ${style.bold(snapshotId)}`,
3096
- data,
3097
- cmd.optsWithGlobals().json
3098
- );
3099
- });
3100
- }
3101
-
3102
- // src/commands/decomposition.ts
3103
- function registerDecomposition(program2) {
3104
- const decomposition = program2.command("decomposition").description(
3105
- "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
3106
- );
3107
- decomposition.addHelpText(
3108
- "after",
3109
- `
3110
- Examples:
3111
- $ sechroom decomposition decompose mem_XXXX
3112
- $ sechroom decomposition execute sug_XXXX
3113
- $ sechroom decomposition publish-run sug_XXXX
3114
- $ sechroom decomposition accept sug_XXXX
3115
- $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
3116
- );
3117
- decomposition.command("decompose <briefId>").description(
3118
- "Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
3119
- ).action(async (briefId, _opts, cmd) => {
3120
- const cfg = resolveConfig(cmd.optsWithGlobals());
3121
- const data = await runApi("Queueing decomposition", async () => {
3122
- const client = await makeClient(cfg);
3123
- return client.POST("/work-briefs/{id}/decompose", {
3124
- params: { path: { id: briefId } },
3125
- body: { id: briefId }
3126
- });
3127
- });
3128
- emitAction(
3129
- `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
3130
- data,
3131
- cmd.optsWithGlobals().json
3132
- );
3133
- });
3134
- decomposition.command("execute <decompositionId>").description(
3135
- "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
3136
- ).action(async (decompositionId, _opts, cmd) => {
3137
- const cfg = resolveConfig(cmd.optsWithGlobals());
3138
- const data = await runApi("Executing decomposition", async () => {
3139
- const client = await makeClient(cfg);
3140
- return client.POST("/decompositions/{id}/execute", {
3141
- params: { path: { id: decompositionId } },
3142
- body: {}
3143
- });
3144
- });
3145
- emitAction(
3146
- `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3147
- data,
3148
- cmd.optsWithGlobals().json
3149
- );
3437
+ emitAction(`created snapshot ${style.bold(data.snapshotId)}`, data, cmd.optsWithGlobals().json);
3150
3438
  });
3151
- decomposition.command("publish-run <decompositionId>").description(
3152
- "Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
3153
- ).action(async (decompositionId, _opts, cmd) => {
3439
+ continuity.command("snapshot-get <id>").description("Fetch a snapshot by id (GET /continuity/snapshots/{id})").action(async (id, _opts, cmd) => {
3154
3440
  const cfg = resolveConfig(cmd.optsWithGlobals());
3155
- const data = await runApi("Publishing context pack", async () => {
3441
+ const data = await runApi("Fetching snapshot", async () => {
3156
3442
  const client = await makeClient(cfg);
3157
- return client.POST("/decompositions/{id}/publish-run", {
3158
- params: { path: { id: decompositionId } },
3159
- body: {}
3160
- });
3443
+ return client.GET("/continuity/snapshots/{id}", { params: { path: { id } } });
3161
3444
  });
3162
- emitAction(
3163
- `published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3164
- data,
3165
- cmd.optsWithGlobals().json
3166
- );
3445
+ emit(data, cmd.optsWithGlobals().json);
3167
3446
  });
3168
- decomposition.command("accept <decompositionId>").description(
3169
- "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
3170
- ).action(async (decompositionId, _opts, cmd) => {
3447
+ continuity.command("snapshots").description("List the caller's own snapshots (GET /me/continuity/snapshots)").option("--scope <scope>", "Filter by scope").option("--lane <laneId>", "Filter by lane id").action(async (opts, cmd) => {
3171
3448
  const cfg = resolveConfig(cmd.optsWithGlobals());
3172
- const data = await runApi("Accepting decomposition", async () => {
3449
+ const data = await runApi("Listing snapshots", async () => {
3173
3450
  const client = await makeClient(cfg);
3174
- return client.POST("/decompositions/{id}/accept", {
3175
- params: { path: { id: decompositionId } },
3176
- body: {}
3451
+ return client.GET("/me/continuity/snapshots", {
3452
+ params: {
3453
+ query: {
3454
+ ...opts.scope ? { scope: opts.scope } : {},
3455
+ ...opts.lane ? { laneId: opts.lane } : {}
3456
+ }
3457
+ }
3177
3458
  });
3178
3459
  });
3179
- emitAction(
3180
- `accepted decomposition ${style.bold(decompositionId)}`,
3181
- data,
3182
- cmd.optsWithGlobals().json
3183
- );
3460
+ emit(data, cmd.optsWithGlobals().json);
3184
3461
  });
3185
- decomposition.command("reject <decompositionId>").description(
3186
- "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
3187
- ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
3462
+ continuity.command("resume-me").description("Resume the caller's own lane (POST /continuity/resume/me)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the bundle").option("--changed-since <iso>", "Only include changes since this ISO timestamp").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3188
3463
  const cfg = resolveConfig(cmd.optsWithGlobals());
3189
- const data = await runApi("Rejecting decomposition", async () => {
3464
+ const data = await runApi("Resuming", async () => {
3190
3465
  const client = await makeClient(cfg);
3191
- return client.POST("/decompositions/{id}/reject", {
3192
- params: { path: { id: decompositionId } },
3193
- body: { reasonText: opts.reason ?? null }
3466
+ return client.POST("/continuity/resume/me", {
3467
+ body: {
3468
+ workspaceId: opts.workspace ?? null,
3469
+ maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3470
+ includeLookingAtMyself: opts.lookingAtMyself ? true : null,
3471
+ changedSince: opts.changedSince ?? null
3472
+ }
3194
3473
  });
3195
3474
  });
3196
- emitAction(
3197
- `rejected decomposition ${style.bold(decompositionId)}`,
3198
- data,
3199
- cmd.optsWithGlobals().json
3200
- );
3201
- });
3202
- }
3203
-
3204
- // src/commands/executor.ts
3205
- import { dirname as dirname8, join as join11 } from "path";
3206
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
3207
- var EXECUTOR_STATE = "executor.json";
3208
- var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
3209
- var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
3210
- var CLAUDE_EXECUTOR_HOOKS = {
3211
- SessionStart: EXECUTOR_PULSE_COMMAND,
3212
- UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
3213
- PreToolUse: EXECUTOR_PULSE_COMMAND,
3214
- Stop: EXECUTOR_PULSE_COMMAND,
3215
- SessionEnd: EXECUTOR_STOP_COMMAND
3216
- };
3217
- var CODEX_EXECUTOR_HOOKS = {
3218
- SessionStart: EXECUTOR_PULSE_COMMAND,
3219
- UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
3220
- PreToolUse: EXECUTOR_PULSE_COMMAND,
3221
- Stop: EXECUTOR_PULSE_COMMAND
3222
- };
3223
- function registerExecutor(program2) {
3224
- const executor = program2.command("executor").description(
3225
- "Register and operate a local Claude Code/Codex executor advertisement"
3226
- );
3227
- executor.command("install").description("Configure this checkout's harness to advertise itself as a WLP executor").option("--connector <id>", "Approved local-session ConnectorDefinition id").option("--instance-key <key>", "Stable executor identity (defaults to .sechroom/lane.json code-lane)").option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option("--capability <key...>", "Capability operation keys claimed by this instance").option("--relay <id>", "Relay identity shared by sibling instances", "sechroom-cli-local").option("--subscription-name <name>", "SignalR delivery binding name", "executor-dispatch").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option("--refresh-after <seconds>", "Minimum age before a hook refreshes", parseInteger, 40).option("-y, --yes", "Non-interactive: accept detected surface and lane defaults", false).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
3228
- const globals = cmd.optsWithGlobals();
3229
- const lane = readSem()?.values["code-lane"];
3230
- const detected = detectHookSurfaces(process.cwd());
3231
- let surface = opts.surface;
3232
- let instanceKey = opts.instanceKey;
3233
- let runtime = opts.runtime;
3234
- let connector = opts.connector;
3235
- let capabilities = opts.capability;
3236
- const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
3237
- if (!opts.yes && canPrompt()) {
3238
- surface = await promptText("Harness surface (claude or codex)?", surface ?? surfaceDefault);
3239
- instanceKey = await promptText("Executor instance key?", instanceKey ?? lane ?? "");
3240
- runtime = await promptText("Runtime (claude-code or codex)?", runtime ?? (surface === "codex" ? "codex" : "claude-code"));
3241
- connector = await promptText("Approved local-session connector id?", connector ?? "");
3242
- const capabilityText = await promptText("Capability keys (comma-separated; blank for none)?", capabilities?.join(",") ?? "");
3243
- capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
3244
- }
3245
- surface ??= surfaceDefault;
3246
- instanceKey ??= lane;
3247
- runtime ??= surface === "codex" ? "codex" : "claude-code";
3248
- if (!connector) fail("executor install requires --connector (or an interactive connector id)");
3249
- if (!instanceKey) fail("no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane");
3250
- if (!opts.yes && !canPrompt()) fail("non-interactive executor install requires --yes");
3251
- parseRuntimeKind(runtime);
3252
- if (!["claude", "codex"].includes(surface)) fail("surface must be claude or codex");
3253
- if (opts.refreshAfter >= opts.ttl) fail("refresh-after must be shorter than the TTL");
3254
- const sem = readSem();
3255
- const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
3256
- const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
3257
- const state = {
3258
- schemaVersion: 1,
3259
- instanceKey,
3260
- runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
3261
- connectorId: connector,
3262
- capabilityKeys: capabilities ?? [],
3263
- relayId: opts.relay,
3264
- subscriptionName: opts.subscriptionName,
3265
- ttlSeconds: opts.ttl,
3266
- refreshAfterSeconds: opts.refreshAfter
3267
- };
3268
- if (!opts.dryRun) {
3269
- mkdirSync9(dirname8(statePath), { recursive: true });
3270
- writeFileSync9(statePath, JSON.stringify(state, null, 2) + "\n");
3271
- ensureStateDirIgnored(checkout);
3272
- }
3273
- const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map((target) => target.dir) : [join11(checkout, ".claude")];
3274
- const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
3275
- const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
3276
- for (const target of hookTargets) {
3277
- const results = surface === "claude" ? [installClaudeCommands(target, CLAUDE_EXECUTOR_HOOKS, opts.dryRun)] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
3278
- for (const result of results) process.stderr.write(describe(result, opts.dryRun) + "\n");
3279
- }
3280
- warnIfSechroomNotOnPath();
3281
- process.stderr.write(style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
3282
- `));
3475
+ emit(data, cmd.optsWithGlobals().json);
3283
3476
  });
3284
- executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
3285
- await drainStdin();
3286
- const located = readExecutorState();
3287
- if (!located) return;
3288
- const { state, path } = located;
3289
- const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
3290
- if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
3477
+ continuity.command("resume-lane <laneId>").description("Resume a specific lane (POST /continuity/resume/lane)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the bundle").option("--changed-since <iso>", "Only include changes since this ISO timestamp").option("--looking-at-myself", "Include the caller's own changes", false).action(async (laneId, opts, cmd) => {
3291
3478
  const cfg = resolveConfig(cmd.optsWithGlobals());
3292
- try {
3293
- let data;
3294
- if (state.instanceId) {
3295
- try {
3296
- data = await refresh(cfg, state.instanceId, state.ttlSeconds);
3297
- } catch {
3298
- data = await registerInstance(cfg, state);
3479
+ const data = await runApi("Resuming lane", async () => {
3480
+ const client = await makeClient(cfg);
3481
+ return client.POST("/continuity/resume/lane", {
3482
+ body: {
3483
+ laneId,
3484
+ workspaceId: opts.workspace ?? null,
3485
+ maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3486
+ includeLookingAtMyself: opts.lookingAtMyself ? true : null,
3487
+ changedSince: opts.changedSince ?? null
3299
3488
  }
3300
- } else {
3301
- data = await registerInstance(cfg, state);
3302
- }
3303
- state.instanceId = data.id;
3304
- state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
3305
- writeFileSync9(path, JSON.stringify(state, null, 2) + "\n");
3306
- } catch {
3307
- }
3308
- });
3309
- executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
3310
- await drainStdin();
3311
- const located = readExecutorState();
3312
- if (!located?.state.instanceId) return;
3313
- try {
3314
- await api(resolveConfig(cmd.optsWithGlobals()), `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`, { method: "DELETE", body: JSON.stringify({}) });
3315
- delete located.state.instanceId;
3316
- delete located.state.lastRefreshAt;
3317
- writeFileSync9(located.path, JSON.stringify(located.state, null, 2) + "\n");
3318
- } catch {
3319
- }
3320
- });
3321
- executor.command("submit-connector").description(
3322
- "Submit a local-session connector definition for governed approval"
3323
- ).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
3324
- "base",
3325
- "dotnet-10"
3326
- ]).action(async (opts, cmd) => {
3327
- const cfg = resolveConfig(cmd.optsWithGlobals());
3328
- const data = await api(cfg, "/connectors/definitions", {
3329
- method: "POST",
3330
- body: JSON.stringify({
3331
- slug: opts.slug,
3332
- displayName: opts.displayName,
3333
- runtimeProfiles: opts.profile,
3334
- connectorKind: "ExecutionRuntime",
3335
- providerKind: "local-session",
3336
- dispatchTransport: parseTransport(opts.transport)
3337
- })
3489
+ });
3338
3490
  });
3339
- emit(data, Boolean(cmd.optsWithGlobals().json));
3340
- if (!cmd.optsWithGlobals().json) {
3341
- process.stderr.write(
3342
- style.dim(
3343
- "approve this connector definition before registering executors\n"
3344
- )
3345
- );
3346
- }
3491
+ emit(data, cmd.optsWithGlobals().json);
3347
3492
  });
3348
- executor.command("register").description(
3349
- "Create/reuse a SignalR binding and register this local executor instance"
3350
- ).requiredOption(
3351
- "--instance-key <key>",
3352
- "Stable key for this concrete session/lane"
3353
- ).requiredOption(
3354
- "--connector <id>",
3355
- "Approved local-session ConnectorDefinition id"
3356
- ).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
3357
- "--relay <id>",
3358
- "Relay identity shared by sibling instances",
3359
- "sechroom-cli-local"
3360
- ).option(
3361
- "--subscription-name <name>",
3362
- "SignalR delivery binding name",
3363
- "executor-dispatch"
3364
- ).option(
3365
- "--capability <key...>",
3366
- "Capability operation keys claimed by this instance"
3367
- ).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
3493
+ continuity.command("changed-since").description("What changed since a timestamp (POST /continuity/changed-since)").requiredOption("--since <iso>", "ISO-8601 timestamp to compare against").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3368
3494
  const cfg = resolveConfig(cmd.optsWithGlobals());
3369
- if (!cfg.workspaceId)
3370
- fail("executor register requires a configured workspaceId");
3371
- const subscription = await api(
3372
- cfg,
3373
- "/me/delivery-subscriptions/signalr",
3374
- {
3375
- method: "POST",
3376
- body: JSON.stringify({
3377
- name: opts.subscriptionName,
3378
- enabled: true,
3379
- filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
3380
- })
3381
- }
3382
- );
3383
- const data = await api(
3384
- cfg,
3385
- "/me/executor-instances",
3386
- {
3387
- method: "POST",
3388
- body: JSON.stringify({
3389
- relayId: opts.relay,
3390
- instanceKey: opts.instanceKey,
3391
- runtimeKind: parseRuntimeKind(opts.runtime),
3392
- activationMode: "Attached",
3393
- deliverySubscriptionId: subscription.id,
3394
- connectorId: opts.connector,
3395
- claimedCapabilityKeys: opts.capability ?? [],
3396
- toolSetRef: opts.toolSetRef ?? null,
3397
- ttlSeconds: opts.ttl
3398
- })
3399
- }
3400
- );
3401
- emit(data, Boolean(cmd.optsWithGlobals().json));
3402
- if (!cmd.optsWithGlobals().json)
3403
- process.stderr.write(
3404
- style.dim(`refresh with: sechroom executor heartbeat ${data.id}
3405
- `)
3406
- );
3407
- });
3408
- executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
3409
- const data = await refresh(
3410
- resolveConfig(cmd.optsWithGlobals()),
3411
- id,
3412
- opts.ttl
3413
- );
3414
- emit(data, Boolean(cmd.optsWithGlobals().json));
3495
+ const data = await runApi("Computing changes", async () => {
3496
+ const client = await makeClient(cfg);
3497
+ return client.POST("/continuity/changed-since", {
3498
+ body: {
3499
+ since: opts.since,
3500
+ includeLookingAtMyself: opts.lookingAtMyself ? true : null
3501
+ }
3502
+ });
3503
+ });
3504
+ emit(data, cmd.optsWithGlobals().json);
3415
3505
  });
3416
- executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
3417
- if (opts.interval >= opts.ttl)
3418
- fail("heartbeat interval must be shorter than the TTL");
3506
+ continuity.command("load-set").description("Derive the active load set (POST /continuity/load-set/derive)").option("--workspace <workspaceId>", "Scope to a workspace").option("--max-artifacts <n>", "Cap artifacts in the load set").option("--looking-at-myself", "Include the caller's own changes", false).action(async (opts, cmd) => {
3419
3507
  const cfg = resolveConfig(cmd.optsWithGlobals());
3420
- await refresh(cfg, id, opts.ttl);
3421
- process.stderr.write(
3422
- style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
3423
- `)
3424
- );
3425
- await holdHeartbeat(async () => {
3426
- await refresh(cfg, id, opts.ttl);
3427
- }, opts.interval * 1e3);
3508
+ const data = await runApi("Deriving load set", async () => {
3509
+ const client = await makeClient(cfg);
3510
+ return client.POST("/continuity/load-set/derive", {
3511
+ body: {
3512
+ workspaceId: opts.workspace ?? null,
3513
+ maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
3514
+ includeLookingAtMyself: opts.lookingAtMyself ? true : null
3515
+ }
3516
+ });
3517
+ });
3518
+ emit(data, cmd.optsWithGlobals().json);
3428
3519
  });
3429
- executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
3520
+ continuity.command("grant <snapshotId>").description("Grant another operator read access (POST /continuity/snapshots/{snapshotId}/grants)").requiredOption("--grantee <userId>", "Sechroom user id being granted read access").option("--source <source>", "Permission-set source kind", "TenantRole").option("--source-id <sourceId>", "Permission-set source id (e.g. a tenant role)", "viewer").option("--valid-from <iso>", "Optional ISO-8601 grant start").option("--valid-to <iso>", "Optional ISO-8601 grant expiry").action(async (snapshotId, opts, cmd) => {
3430
3521
  const cfg = resolveConfig(cmd.optsWithGlobals());
3431
- const data = await api(
3432
- cfg,
3433
- `/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
3522
+ const data = await runApi("Minting grant", async () => {
3523
+ const client = await makeClient(cfg);
3524
+ return client.POST("/continuity/snapshots/{snapshotId}/grants", {
3525
+ params: { path: { snapshotId } },
3526
+ body: {
3527
+ userId: opts.grantee,
3528
+ kind: "Allow",
3529
+ source: opts.source,
3530
+ sourceId: opts.sourceId,
3531
+ ...opts.validFrom ? { validFrom: opts.validFrom } : {},
3532
+ ...opts.validTo ? { validTo: opts.validTo } : {}
3533
+ }
3534
+ });
3535
+ });
3536
+ emitAction(
3537
+ `granted ${style.bold(data.userId)} read on ${style.bold(snapshotId)} ${style.dim(`(grant ${data.grantId})`)}`,
3538
+ data,
3539
+ cmd.optsWithGlobals().json
3434
3540
  );
3435
- emit(data, Boolean(cmd.optsWithGlobals().json));
3436
3541
  });
3437
- executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
3542
+ continuity.command("revoke-grant <snapshotId> <grantId>").description("Revoke a grant (DELETE /continuity/snapshots/{snapshotId}/grants/{grantId})").action(async (snapshotId, grantId, _opts, cmd) => {
3438
3543
  const cfg = resolveConfig(cmd.optsWithGlobals());
3439
- const data = await api(
3440
- cfg,
3441
- `/me/executor-instances/${encodeURIComponent(id)}`,
3442
- {
3443
- method: "DELETE",
3444
- body: JSON.stringify({})
3445
- }
3544
+ const data = await runApi("Revoking grant", async () => {
3545
+ const client = await makeClient(cfg);
3546
+ return client.DELETE("/continuity/snapshots/{snapshotId}/grants/{grantId}", {
3547
+ params: { path: { snapshotId, grantId } },
3548
+ body: {}
3549
+ });
3550
+ });
3551
+ emitAction(
3552
+ `revoked grant ${style.bold(grantId)} on ${style.bold(snapshotId)}`,
3553
+ data,
3554
+ cmd.optsWithGlobals().json
3446
3555
  );
3447
- emit(data, Boolean(cmd.optsWithGlobals().json));
3448
3556
  });
3449
3557
  }
3450
- function parseRuntimeKind(value) {
3451
- switch (value.trim().toLowerCase()) {
3452
- case "claude":
3453
- case "claude-code":
3454
- return "ClaudeCode";
3455
- case "codex":
3456
- return "Codex";
3457
- default:
3458
- return fail("runtime must be claude-code or codex");
3459
- }
3460
- }
3461
- function parseTransport(value) {
3462
- switch (value.trim().toLowerCase()) {
3463
- case "push":
3464
- return "Push";
3465
- case "pull":
3466
- return "Pull";
3467
- default:
3468
- return fail("transport must be push or pull");
3469
- }
3470
- }
3471
- async function refresh(cfg, id, ttlSeconds) {
3472
- return api(
3473
- cfg,
3474
- `/me/executor-instances/${encodeURIComponent(id)}/refresh`,
3475
- {
3476
- method: "POST",
3477
- body: JSON.stringify({ ttlSeconds })
3478
- }
3558
+
3559
+ // src/commands/decomposition.ts
3560
+ function registerDecomposition(program2) {
3561
+ const decomposition = program2.command("decomposition").description(
3562
+ "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
3479
3563
  );
3480
- }
3481
- async function registerInstance(cfg, state) {
3482
- if (!cfg.workspaceId) fail("executor registration requires a configured workspaceId");
3483
- const subscription = await api(cfg, "/me/delivery-subscriptions/signalr", {
3484
- method: "POST",
3485
- body: JSON.stringify({ name: state.subscriptionName, enabled: true, filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] } })
3564
+ decomposition.addHelpText(
3565
+ "after",
3566
+ `
3567
+ Examples:
3568
+ $ sechroom decomposition decompose mem_XXXX
3569
+ $ sechroom decomposition execute sug_XXXX
3570
+ $ sechroom decomposition publish-run sug_XXXX
3571
+ $ sechroom decomposition accept sug_XXXX
3572
+ $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
3573
+ );
3574
+ decomposition.command("decompose <briefId>").description(
3575
+ "Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
3576
+ ).action(async (briefId, _opts, cmd) => {
3577
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3578
+ const data = await runApi("Queueing decomposition", async () => {
3579
+ const client = await makeClient(cfg);
3580
+ return client.POST("/work-briefs/{id}/decompose", {
3581
+ params: { path: { id: briefId } },
3582
+ body: { id: briefId }
3583
+ });
3584
+ });
3585
+ emitAction(
3586
+ `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
3587
+ data,
3588
+ cmd.optsWithGlobals().json
3589
+ );
3486
3590
  });
3487
- return api(cfg, "/me/executor-instances", {
3488
- method: "POST",
3489
- body: JSON.stringify({ relayId: state.relayId, instanceKey: state.instanceKey, runtimeKind: parseRuntimeKind(state.runtime), activationMode: "Attached", deliverySubscriptionId: subscription.id, connectorId: state.connectorId, claimedCapabilityKeys: state.capabilityKeys, toolSetRef: null, ttlSeconds: state.ttlSeconds })
3591
+ decomposition.command("execute <decompositionId>").description(
3592
+ "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
3593
+ ).action(async (decompositionId, _opts, cmd) => {
3594
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3595
+ const data = await runApi("Executing decomposition", async () => {
3596
+ const client = await makeClient(cfg);
3597
+ return client.POST("/decompositions/{id}/execute", {
3598
+ params: { path: { id: decompositionId } },
3599
+ body: {}
3600
+ });
3601
+ });
3602
+ emitAction(
3603
+ `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3604
+ data,
3605
+ cmd.optsWithGlobals().json
3606
+ );
3490
3607
  });
3491
- }
3492
- function readExecutorState(start = process.cwd()) {
3493
- const semPath = resolveSemPathForRead(start);
3494
- const sem = semPath ? readSem(semPath) : void 0;
3495
- const path = join11(sem ? dirname8(sem.path) : join11(start, ".sechroom"), EXECUTOR_STATE);
3496
- if (!existsSync9(path)) return void 0;
3497
- return { state: JSON.parse(readFileSync8(path, "utf8")), path };
3498
- }
3499
- async function drainStdin() {
3500
- if (process.stdin.isTTY) return;
3501
- for await (const _chunk of process.stdin) {
3502
- }
3503
- }
3504
- async function api(cfg, path, init) {
3505
- const token = await requireToken(cfg);
3506
- const response = await fetch(`${cfg.baseUrl}${path}`, {
3507
- ...init,
3508
- headers: {
3509
- authorization: `Bearer ${token}`,
3510
- tenant: cfg.tenant,
3511
- "content-type": "application/json",
3512
- "x-sechroom-surface": "cli"
3513
- }
3608
+ decomposition.command("publish-run <decompositionId>").description(
3609
+ "Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
3610
+ ).action(async (decompositionId, _opts, cmd) => {
3611
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3612
+ const data = await runApi("Publishing context pack", async () => {
3613
+ const client = await makeClient(cfg);
3614
+ return client.POST("/decompositions/{id}/publish-run", {
3615
+ params: { path: { id: decompositionId } },
3616
+ body: {}
3617
+ });
3618
+ });
3619
+ emitAction(
3620
+ `published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
3621
+ data,
3622
+ cmd.optsWithGlobals().json
3623
+ );
3514
3624
  });
3515
- if (!response.ok)
3516
- fail(
3517
- `${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
3625
+ decomposition.command("accept <decompositionId>").description(
3626
+ "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
3627
+ ).action(async (decompositionId, _opts, cmd) => {
3628
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3629
+ const data = await runApi("Accepting decomposition", async () => {
3630
+ const client = await makeClient(cfg);
3631
+ return client.POST("/decompositions/{id}/accept", {
3632
+ params: { path: { id: decompositionId } },
3633
+ body: {}
3634
+ });
3635
+ });
3636
+ emitAction(
3637
+ `accepted decomposition ${style.bold(decompositionId)}`,
3638
+ data,
3639
+ cmd.optsWithGlobals().json
3640
+ );
3641
+ });
3642
+ decomposition.command("reject <decompositionId>").description(
3643
+ "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
3644
+ ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
3645
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3646
+ const data = await runApi("Rejecting decomposition", async () => {
3647
+ const client = await makeClient(cfg);
3648
+ return client.POST("/decompositions/{id}/reject", {
3649
+ params: { path: { id: decompositionId } },
3650
+ body: { reasonText: opts.reason ?? null }
3651
+ });
3652
+ });
3653
+ emitAction(
3654
+ `rejected decomposition ${style.bold(decompositionId)}`,
3655
+ data,
3656
+ cmd.optsWithGlobals().json
3518
3657
  );
3519
- return response.json();
3520
- }
3521
- function parseInteger(value) {
3522
- const parsed = Number.parseInt(value, 10);
3523
- if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
3524
- return parsed;
3525
- }
3526
- function holdHeartbeat(tick, intervalMs) {
3527
- return new Promise((resolve3, reject) => {
3528
- const timer = setInterval(() => void tick().catch(reject), intervalMs);
3529
- const stop = () => {
3530
- clearInterval(timer);
3531
- resolve3();
3532
- };
3533
- process.once("SIGINT", stop);
3534
- process.once("SIGTERM", stop);
3535
3658
  });
3536
3659
  }
3537
3660
 
@@ -5799,23 +5922,23 @@ Examples:
5799
5922
  });
5800
5923
  emit(data, cmd.optsWithGlobals().json);
5801
5924
  });
5802
- suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{id}/accept)").action(async (id, _opts, cmd) => {
5925
+ suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)").action(async (id, _opts, cmd) => {
5803
5926
  const cfg = resolveConfig(cmd.optsWithGlobals());
5804
5927
  const data = await runApi("Accepting suggestion", async () => {
5805
5928
  const client = await makeClient(cfg);
5806
- return client.POST("/relationship-suggestions/{id}/accept", {
5807
- params: { path: { id } },
5929
+ return client.POST("/relationship-suggestions/{instanceId}/accept", {
5930
+ params: { path: { instanceId: id } },
5808
5931
  body: {}
5809
5932
  });
5810
5933
  });
5811
5934
  emitAction(`accepted suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
5812
5935
  });
5813
- suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{id}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
5936
+ suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
5814
5937
  const cfg = resolveConfig(cmd.optsWithGlobals());
5815
5938
  const data = await runApi("Rejecting suggestion", async () => {
5816
5939
  const client = await makeClient(cfg);
5817
- return client.POST("/relationship-suggestions/{id}/reject", {
5818
- params: { path: { id } },
5940
+ return client.POST("/relationship-suggestions/{instanceId}/reject", {
5941
+ params: { path: { instanceId: id } },
5819
5942
  body: {
5820
5943
  reason: opts.reason ?? null,
5821
5944
  ...opts.reasonCode ? { reasonCode: opts.reasonCode } : {}
@@ -6790,6 +6913,56 @@ Examples:
6790
6913
  });
6791
6914
  }
6792
6915
 
6916
+ // src/commands/work-brief.ts
6917
+ function registerWorkBrief(program2) {
6918
+ const workBrief = program2.command("work-brief").description("Control an active work brief run");
6919
+ workBrief.addHelpText(
6920
+ "after",
6921
+ `
6922
+ Examples:
6923
+ $ sechroom work-brief pause mem_XXXX --reason-code operator-hold --source claude-code-chris
6924
+ $ sechroom work-brief resume mem_XXXX --reason-code operator-resume --source claude-code-chris --reason "Ready to continue"
6925
+ $ sechroom work-brief cancel mem_XXXX --reason-code operator-stopped --source claude-code-chris --reason "Work no longer required"`
6926
+ );
6927
+ registerLifecycleAction(workBrief, "pause");
6928
+ registerLifecycleAction(workBrief, "resume");
6929
+ registerLifecycleAction(workBrief, "cancel");
6930
+ }
6931
+ function registerLifecycleAction(workBrief, action) {
6932
+ const presentParticiple = action === "pause" ? "Pausing" : action === "resume" ? "Resuming" : "Cancelling";
6933
+ const pastTense = action === "pause" ? "paused" : action === "resume" ? "resumed" : "cancelled";
6934
+ workBrief.command(`${action} <briefId>`).description(`${capitalize(action)} an active work brief run`).requiredOption(
6935
+ "--reason-code <code>",
6936
+ "Stable machine-readable reason code"
6937
+ ).requiredOption(
6938
+ "--source <source>",
6939
+ "Calling surface or lane recorded in the audit"
6940
+ ).option("--reason <text>", "Optional human-readable reason").action(async (briefId, opts, cmd) => {
6941
+ const globals = cmd.optsWithGlobals();
6942
+ const cfg = resolveConfig(globals);
6943
+ const data = await runApi(`${presentParticiple} work brief`, async () => {
6944
+ const client = await makeClient(cfg);
6945
+ return client.POST("/work-briefs/{id}/lifecycle", {
6946
+ params: { path: { id: briefId } },
6947
+ body: {
6948
+ action,
6949
+ reasonCode: opts.reasonCode,
6950
+ source: opts.source,
6951
+ reasonText: opts.reason ?? null
6952
+ }
6953
+ });
6954
+ });
6955
+ emitAction(
6956
+ `${pastTense} work brief ${style.bold(briefId)} \u2192 ${data.outcome}`,
6957
+ data,
6958
+ globals.json
6959
+ );
6960
+ });
6961
+ }
6962
+ function capitalize(value) {
6963
+ return value.charAt(0).toUpperCase() + value.slice(1);
6964
+ }
6965
+
6793
6966
  // src/index.ts
6794
6967
  function resolveVersion() {
6795
6968
  try {
@@ -6952,6 +7125,7 @@ registerLookup(program);
6952
7125
  registerRelationships(program);
6953
7126
  registerWorkspace(program);
6954
7127
  registerProject(program);
7128
+ registerWorkBrief(program);
6955
7129
  registerDecomposition(program);
6956
7130
  registerExecutor(program);
6957
7131
  registerClose(program);