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