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