@sechroom/cli 2026.7.7 → 2026.7.8-rc.4b6e8ecc
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/dist/index.js +1090 -165
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -439,6 +439,9 @@ var quiet = false;
|
|
|
439
439
|
function setQuiet(q) {
|
|
440
440
|
quiet = q;
|
|
441
441
|
}
|
|
442
|
+
function isQuiet() {
|
|
443
|
+
return quiet;
|
|
444
|
+
}
|
|
442
445
|
function colorOn() {
|
|
443
446
|
return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
|
|
444
447
|
}
|
|
@@ -615,6 +618,22 @@ function emitAction(summary, data, json) {
|
|
|
615
618
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
616
619
|
`);
|
|
617
620
|
}
|
|
621
|
+
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
622
|
+
function isGovernanceQueued(body) {
|
|
623
|
+
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
624
|
+
}
|
|
625
|
+
function formatQueuedForApproval(body, json) {
|
|
626
|
+
if (json) return JSON.stringify(body) + "\n";
|
|
627
|
+
const b = body ?? {};
|
|
628
|
+
const detail = typeof b.detail === "string" ? b.detail : void 0;
|
|
629
|
+
const requestId = typeof b.requestId === "string" ? b.requestId : void 0;
|
|
630
|
+
const message = detail ?? "This change requires operator approval before it takes effect.";
|
|
631
|
+
let out = `${warn("\u29D7")} Pending approval \u2014 ${message}
|
|
632
|
+
`;
|
|
633
|
+
if (requestId) out += ` ${style.dim(`request: ${requestId}`)}
|
|
634
|
+
`;
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
618
637
|
async function runApi(label, fn) {
|
|
619
638
|
const s = spinner(label);
|
|
620
639
|
let res;
|
|
@@ -624,6 +643,12 @@ async function runApi(label, fn) {
|
|
|
624
643
|
s.fail();
|
|
625
644
|
fail(err2);
|
|
626
645
|
}
|
|
646
|
+
const queuedBody = res.data ?? res.error;
|
|
647
|
+
if (res.response?.status === 202 && isGovernanceQueued(queuedBody)) {
|
|
648
|
+
s.stop();
|
|
649
|
+
process.stdout.write(formatQueuedForApproval(queuedBody, isQuiet()));
|
|
650
|
+
process.exit(0);
|
|
651
|
+
}
|
|
627
652
|
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
628
653
|
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
629
654
|
s.fail();
|
|
@@ -632,8 +657,32 @@ async function runApi(label, fn) {
|
|
|
632
657
|
s.succeed();
|
|
633
658
|
return res.data;
|
|
634
659
|
}
|
|
660
|
+
function formatFailureMessage(error) {
|
|
661
|
+
let msg;
|
|
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) {
|
|
668
|
+
const problem = error;
|
|
669
|
+
msg = String(problem.title);
|
|
670
|
+
if (problem.errors && typeof problem.errors === "object") {
|
|
671
|
+
const detail = Object.entries(problem.errors).flatMap(
|
|
672
|
+
([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
|
|
673
|
+
);
|
|
674
|
+
if (detail.length > 0) msg += `
|
|
675
|
+
${detail.join("\n")}`;
|
|
676
|
+
}
|
|
677
|
+
} else if (typeof error === "object" && error !== null) {
|
|
678
|
+
msg = JSON.stringify(error);
|
|
679
|
+
} else {
|
|
680
|
+
msg = String(error);
|
|
681
|
+
}
|
|
682
|
+
return msg;
|
|
683
|
+
}
|
|
635
684
|
function fail(error) {
|
|
636
|
-
const msg =
|
|
685
|
+
const msg = formatFailureMessage(error);
|
|
637
686
|
process.stderr.write(`error: ${msg}
|
|
638
687
|
`);
|
|
639
688
|
process.exit(1);
|
|
@@ -1061,14 +1110,23 @@ function bodyOf(row) {
|
|
|
1061
1110
|
const m = row?.item ?? row;
|
|
1062
1111
|
return m?.text ?? m?.Text ?? "";
|
|
1063
1112
|
}
|
|
1113
|
+
function idOf(row) {
|
|
1114
|
+
const m = row?.item ?? row;
|
|
1115
|
+
return String(m?.id ?? m?.memoryId ?? "unknown");
|
|
1116
|
+
}
|
|
1064
1117
|
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
1065
1118
|
const out = /* @__PURE__ */ new Map();
|
|
1066
1119
|
for (const row of rows ?? []) {
|
|
1067
1120
|
const tags = tagsOf(row);
|
|
1068
1121
|
if (!tags.includes(roleTag)) continue;
|
|
1069
|
-
if (
|
|
1122
|
+
if (!tags.includes(`target:${surface}`)) continue;
|
|
1070
1123
|
const name = tagValue(tags, namePrefix);
|
|
1071
1124
|
if (!name) continue;
|
|
1125
|
+
if (out.has(name)) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`Ambiguous ${source} ${roleTag} '${name}' for target:${surface}: ${idOf(row)} duplicates another eligible component. Resolve the duplicate before installing.`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1072
1130
|
out.set(name, { name, body: bodyOf(row), source });
|
|
1073
1131
|
}
|
|
1074
1132
|
return out;
|
|
@@ -1091,25 +1149,41 @@ function resolveReferences(systemRows, personalRows, surface) {
|
|
|
1091
1149
|
}
|
|
1092
1150
|
|
|
1093
1151
|
// src/setup/skill-resolution-io.ts
|
|
1094
|
-
var AGENT_TARGET = {
|
|
1152
|
+
var AGENT_TARGET = {
|
|
1153
|
+
"claude-code": "claude-agent",
|
|
1154
|
+
"gpt-codex": "gpt-codex-agent"
|
|
1155
|
+
};
|
|
1095
1156
|
function agentTargetFor(surface) {
|
|
1096
1157
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1097
1158
|
}
|
|
1098
1159
|
async function fetchFeedRows(cfg, workspaceId) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1160
|
+
const client = await makeClient(cfg);
|
|
1161
|
+
const rows = [];
|
|
1162
|
+
let cursor;
|
|
1163
|
+
do {
|
|
1164
|
+
const { data, error, response } = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
1102
1165
|
params: {
|
|
1103
1166
|
path: { workspaceId },
|
|
1104
1167
|
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
1105
1168
|
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
1106
|
-
query: {
|
|
1169
|
+
query: {
|
|
1170
|
+
limit: 200,
|
|
1171
|
+
cascadeWorkspaces: true,
|
|
1172
|
+
includeText: true,
|
|
1173
|
+
...cursor ? { cursor } : {}
|
|
1174
|
+
}
|
|
1107
1175
|
}
|
|
1108
|
-
})
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1176
|
+
});
|
|
1177
|
+
if (error || !response.ok || !data) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`Could not read operator-skill catalogue from ${workspaceId}: HTTP ${response.status}.`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const feed = data;
|
|
1183
|
+
rows.push(...feed.results ?? feed.Results ?? []);
|
|
1184
|
+
cursor = feed.nextCursor ?? feed.NextCursor ?? void 0;
|
|
1185
|
+
} while (cursor);
|
|
1186
|
+
return rows;
|
|
1113
1187
|
}
|
|
1114
1188
|
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
1115
1189
|
const [systemRows, personalRows] = await Promise.all([
|
|
@@ -1129,7 +1203,10 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1129
1203
|
}
|
|
1130
1204
|
|
|
1131
1205
|
// src/setup/materialise.ts
|
|
1132
|
-
var CLIENT_SURFACE =
|
|
1206
|
+
var CLIENT_SURFACE = {
|
|
1207
|
+
claude: "claude-code",
|
|
1208
|
+
codex: "gpt-codex"
|
|
1209
|
+
};
|
|
1133
1210
|
function writeSkills(dir, skills, surface) {
|
|
1134
1211
|
const written = [];
|
|
1135
1212
|
for (const s of skills) {
|
|
@@ -1140,12 +1217,58 @@ function writeSkills(dir, skills, surface) {
|
|
|
1140
1217
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
1141
1218
|
return written;
|
|
1142
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
|
+
}
|
|
1143
1264
|
function writeAgents(dir, agents, surface) {
|
|
1144
1265
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1145
1266
|
const written = [];
|
|
1146
1267
|
for (const a of agents) {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
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);
|
|
1149
1272
|
written.push(file);
|
|
1150
1273
|
}
|
|
1151
1274
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1162,11 +1285,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
|
1162
1285
|
}
|
|
1163
1286
|
return refs.map((r) => r.name);
|
|
1164
1287
|
}
|
|
1165
|
-
var SKILL_SPEC = {
|
|
1166
|
-
|
|
1288
|
+
var SKILL_SPEC = {
|
|
1289
|
+
kind: "skill",
|
|
1290
|
+
dir: skillsDir,
|
|
1291
|
+
resolve: resolveSkillSet,
|
|
1292
|
+
write: writeSkills,
|
|
1293
|
+
supportsCodex: true
|
|
1294
|
+
};
|
|
1295
|
+
var AGENT_SPEC = {
|
|
1296
|
+
kind: "agent",
|
|
1297
|
+
dir: agentsDir,
|
|
1298
|
+
resolve: resolveAgentSet,
|
|
1299
|
+
write: writeAgents,
|
|
1300
|
+
supportsCodex: true
|
|
1301
|
+
};
|
|
1167
1302
|
function scopeOf(opts) {
|
|
1168
1303
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1169
1304
|
}
|
|
1305
|
+
function parseClient(value) {
|
|
1306
|
+
if (value == null) return void 0;
|
|
1307
|
+
if (value === "claude" || value === "codex" || value === "all") return value;
|
|
1308
|
+
throw new Error(`--client must be 'claude', 'codex', or 'all' (got '${value}')`);
|
|
1309
|
+
}
|
|
1310
|
+
function clientSelection(spec, g, raw) {
|
|
1311
|
+
if (!spec.supportsCodex) return "claude";
|
|
1312
|
+
const explicit = parseClient(raw);
|
|
1313
|
+
if (explicit) return explicit;
|
|
1314
|
+
const claudeConfigured = Boolean(g.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
1315
|
+
const codexConfigured = Boolean(g.codexHome || process.env.CODEX_HOME);
|
|
1316
|
+
if (claudeConfigured || codexConfigured) {
|
|
1317
|
+
if (claudeConfigured && codexConfigured) return "all";
|
|
1318
|
+
return codexConfigured ? "codex" : "claude";
|
|
1319
|
+
}
|
|
1320
|
+
const claudeDetected = resolveClaudeTargets({})[0]?.dir;
|
|
1321
|
+
const codexDetected = resolveCodexHomes({})[0];
|
|
1322
|
+
const hasClaude = Boolean(claudeDetected && existsSync3(claudeDetected));
|
|
1323
|
+
const hasCodex = Boolean(codexDetected && existsSync3(codexDetected));
|
|
1324
|
+
if (hasClaude && hasCodex) return "all";
|
|
1325
|
+
if (hasCodex) return "codex";
|
|
1326
|
+
return "claude";
|
|
1327
|
+
}
|
|
1328
|
+
function targetsFor(spec, g, scope, selection) {
|
|
1329
|
+
const clients = selection === "all" ? ["claude", "codex"] : [selection];
|
|
1330
|
+
if (scope === "project" && clients.includes("codex")) {
|
|
1331
|
+
throw new Error("Codex skills have no supported project scope; use --scope global or --client claude.");
|
|
1332
|
+
}
|
|
1333
|
+
const targets = [];
|
|
1334
|
+
for (const client of clients) {
|
|
1335
|
+
if (client === "claude") {
|
|
1336
|
+
targets.push(...resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() }).map((target) => ({
|
|
1337
|
+
client,
|
|
1338
|
+
surface: CLIENT_SURFACE[client],
|
|
1339
|
+
dir: spec.dir(target.dir),
|
|
1340
|
+
label: target.label
|
|
1341
|
+
})));
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
targets.push(...resolveCodexHomes({ override: g.codexHome, scope }).map((home) => ({
|
|
1345
|
+
client,
|
|
1346
|
+
surface: CLIENT_SURFACE[client],
|
|
1347
|
+
dir: spec.dir(home),
|
|
1348
|
+
label: home
|
|
1349
|
+
})));
|
|
1350
|
+
}
|
|
1351
|
+
return targets;
|
|
1352
|
+
}
|
|
1170
1353
|
async function runInstall(spec, cmd, opts) {
|
|
1171
1354
|
const g = cmd.optsWithGlobals();
|
|
1172
1355
|
let scope;
|
|
@@ -1178,31 +1361,52 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1178
1361
|
const cfg = resolveConfig(g);
|
|
1179
1362
|
const dryRun = Boolean(opts.dryRun);
|
|
1180
1363
|
const json = Boolean(g.json || opts.json);
|
|
1181
|
-
|
|
1364
|
+
let selection;
|
|
1365
|
+
let targets;
|
|
1366
|
+
try {
|
|
1367
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1368
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1369
|
+
} catch (err2) {
|
|
1370
|
+
return fail(err2.message);
|
|
1371
|
+
}
|
|
1182
1372
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
1183
1373
|
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
1184
|
-
const items = spec.resolve(rows, CLIENT_SURFACE);
|
|
1185
|
-
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
|
|
1186
1374
|
const results = targets.map((t) => {
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
}
|
|
1380
|
+
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1381
|
+
const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
|
|
1382
|
+
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1383
|
+
return {
|
|
1384
|
+
client: t.client,
|
|
1385
|
+
surface: t.surface,
|
|
1386
|
+
dir: t.dir,
|
|
1387
|
+
label: t.label,
|
|
1388
|
+
available: items.length,
|
|
1389
|
+
references: refs.length,
|
|
1390
|
+
items: items.map(({ name, source }) => ({ name, source })),
|
|
1391
|
+
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1392
|
+
written,
|
|
1393
|
+
refsWritten
|
|
1394
|
+
};
|
|
1191
1395
|
});
|
|
1192
|
-
if (json) return emit({ kind: spec.kind,
|
|
1193
|
-
if (
|
|
1396
|
+
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
1397
|
+
if (results.every((r) => r.available === 0)) {
|
|
1194
1398
|
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
1195
1399
|
return;
|
|
1196
1400
|
}
|
|
1197
1401
|
for (const r of results) {
|
|
1198
1402
|
console.log(
|
|
1199
|
-
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
|
|
1403
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) for ${r.client} ${style.dim("\u2192")} ${r.dir}`
|
|
1200
1404
|
);
|
|
1201
1405
|
if (r.refsWritten.length)
|
|
1202
1406
|
console.log(
|
|
1203
1407
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1204
1408
|
);
|
|
1205
|
-
if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1409
|
+
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1206
1410
|
}
|
|
1207
1411
|
}
|
|
1208
1412
|
function runList(spec, cmd, opts) {
|
|
@@ -1214,16 +1418,22 @@ function runList(spec, cmd, opts) {
|
|
|
1214
1418
|
return fail(err2.message);
|
|
1215
1419
|
}
|
|
1216
1420
|
const json = Boolean(g.json || opts.json);
|
|
1217
|
-
|
|
1421
|
+
let selection;
|
|
1422
|
+
let targets;
|
|
1423
|
+
try {
|
|
1424
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1425
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1426
|
+
} catch (err2) {
|
|
1427
|
+
return fail(err2.message);
|
|
1428
|
+
}
|
|
1218
1429
|
const out = targets.map((t) => {
|
|
1219
|
-
const
|
|
1220
|
-
const lock = readSkillsLock(dir);
|
|
1430
|
+
const lock = readSkillsLock(t.dir);
|
|
1221
1431
|
const entries = Object.entries(lock).flatMap(
|
|
1222
|
-
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(dir, name)) }))
|
|
1432
|
+
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(t.dir, name)) }))
|
|
1223
1433
|
);
|
|
1224
|
-
return { dir, label: t.label, entries };
|
|
1434
|
+
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1225
1435
|
});
|
|
1226
|
-
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
1436
|
+
if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
|
|
1227
1437
|
let any = false;
|
|
1228
1438
|
for (const t of out) {
|
|
1229
1439
|
if (t.entries.length === 0) continue;
|
|
@@ -1246,33 +1456,39 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1246
1456
|
return fail(err2.message);
|
|
1247
1457
|
}
|
|
1248
1458
|
const json = Boolean(g.json || opts.json);
|
|
1249
|
-
|
|
1459
|
+
let selection;
|
|
1460
|
+
let targets;
|
|
1461
|
+
try {
|
|
1462
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1463
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1464
|
+
} catch (err2) {
|
|
1465
|
+
return fail(err2.message);
|
|
1466
|
+
}
|
|
1250
1467
|
const cleaned = [];
|
|
1251
1468
|
const missing = [];
|
|
1252
1469
|
for (const t of targets) {
|
|
1253
|
-
const
|
|
1254
|
-
const lock = readSkillsLock(dir);
|
|
1470
|
+
const lock = readSkillsLock(t.dir);
|
|
1255
1471
|
const entry = lock[slug];
|
|
1256
1472
|
if (!entry) {
|
|
1257
|
-
missing.push(join4(dir, SKILLS_LOCK));
|
|
1473
|
+
missing.push(join4(t.dir, SKILLS_LOCK));
|
|
1258
1474
|
continue;
|
|
1259
1475
|
}
|
|
1260
1476
|
const removed = [];
|
|
1261
1477
|
for (const name of entry.skills) {
|
|
1262
|
-
const p = join4(dir, name);
|
|
1478
|
+
const p = join4(t.dir, name);
|
|
1263
1479
|
if (existsSync3(p)) {
|
|
1264
1480
|
rmSync2(p, { recursive: true, force: true });
|
|
1265
1481
|
removed.push(name);
|
|
1266
1482
|
}
|
|
1267
1483
|
}
|
|
1268
1484
|
delete lock[slug];
|
|
1269
|
-
writeSkillsLock(dir, lock);
|
|
1270
|
-
cleaned.push({ dir, removed });
|
|
1485
|
+
writeSkillsLock(t.dir, lock);
|
|
1486
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
|
|
1271
1487
|
}
|
|
1272
1488
|
if (cleaned.length === 0) {
|
|
1273
1489
|
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
1274
1490
|
}
|
|
1275
|
-
if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
|
|
1491
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug, cleaned, missing }, true);
|
|
1276
1492
|
for (const c of cleaned) {
|
|
1277
1493
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
1278
1494
|
}
|
|
@@ -1286,17 +1502,19 @@ function registerAgents(program2) {
|
|
|
1286
1502
|
`
|
|
1287
1503
|
Examples:
|
|
1288
1504
|
$ sechroom agents install materialise your installed agents to ~/.claude/agents
|
|
1505
|
+
$ sechroom agents install --client codex materialise native TOML agents to ~/.codex/agents
|
|
1289
1506
|
$ sechroom agents install --scope project write them to ./.claude/agents instead
|
|
1290
1507
|
$ sechroom agents install --claude-config-dir ~/.claude-work target another instance
|
|
1291
1508
|
$ sechroom agents list what's materialised on disk
|
|
1292
1509
|
$ sechroom agents clean remove the materialised agent files
|
|
1293
1510
|
|
|
1294
|
-
Agents are resolved from the agent target (target:claude-agent
|
|
1295
|
-
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).`
|
|
1296
1514
|
);
|
|
1297
|
-
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));
|
|
1298
|
-
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));
|
|
1299
|
-
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));
|
|
1300
1518
|
}
|
|
1301
1519
|
|
|
1302
1520
|
// src/commands/channel.ts
|
|
@@ -1388,15 +1606,24 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1388
1606
|
SessionStart: "sechroom hook session-start",
|
|
1389
1607
|
PreCompact: "sechroom hook pre-compact",
|
|
1390
1608
|
SessionEnd: "sechroom hook session-end",
|
|
1391
|
-
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report.
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1609
|
+
// WLP telemetry tap (D-WLP-10 + FR-352 Tier 1) — per-turn executor self-report. The one
|
|
1610
|
+
// `telemetry hook` verb dispatches on hook_event_name: Stop/SubagentStop → parsed (token/context) +
|
|
1611
|
+
// terminal (turn end), Notification/PermissionDenied → approval. No-op (exit 0) unless this checkout
|
|
1612
|
+
// is bound via `sechroom telemetry bind`, so it's safe to wire for every Claude install; an event a
|
|
1613
|
+
// given Claude Code version doesn't know is inert (never fires). Claude-only.
|
|
1614
|
+
Stop: "sechroom telemetry hook",
|
|
1615
|
+
SubagentStop: "sechroom telemetry hook",
|
|
1616
|
+
Notification: "sechroom telemetry hook",
|
|
1617
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
1395
1618
|
};
|
|
1396
1619
|
var CODEX_HOOK_COMMANDS = {
|
|
1397
1620
|
SessionStart: "sechroom hook session-start",
|
|
1621
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1398
1622
|
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1399
1623
|
};
|
|
1624
|
+
function hookCommandsForSurface(surface) {
|
|
1625
|
+
return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
|
|
1626
|
+
}
|
|
1400
1627
|
function hasHookCommand(config2, event, command) {
|
|
1401
1628
|
const groups = config2.hooks?.[event] ?? [];
|
|
1402
1629
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
@@ -1432,6 +1659,12 @@ function installHooksJson(path, commands, dryRun) {
|
|
|
1432
1659
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1433
1660
|
return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
|
|
1434
1661
|
}
|
|
1662
|
+
function installCodexCommands(codexHome, commands, dryRun) {
|
|
1663
|
+
return [
|
|
1664
|
+
installHooksJson(join6(codexHome, "hooks.json"), commands, dryRun),
|
|
1665
|
+
installCodexFeatureFlag(join6(codexHome, "config.toml"), dryRun)
|
|
1666
|
+
];
|
|
1667
|
+
}
|
|
1435
1668
|
function ensureCodexFeaturesHooks(content) {
|
|
1436
1669
|
const lines = content.split("\n");
|
|
1437
1670
|
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
@@ -1534,7 +1767,10 @@ function registerChannel(program2) {
|
|
|
1534
1767
|
"--tag <tag...>",
|
|
1535
1768
|
"Tag(s) the event must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1536
1769
|
["kind:task"]
|
|
1537
|
-
).option("--workspace <wsp...>", "Restrict to these workspace id(s)")
|
|
1770
|
+
).option("--workspace <wsp...>", "Restrict to these workspace id(s)").option(
|
|
1771
|
+
"--executor-instance <id>",
|
|
1772
|
+
"Join the exact executor-instance SignalR group"
|
|
1773
|
+
);
|
|
1538
1774
|
withFilterOpts(
|
|
1539
1775
|
channel.command("connect").description(
|
|
1540
1776
|
"Register a SignalR subscription and stream matched events to stdout"
|
|
@@ -1552,7 +1788,7 @@ function registerChannel(program2) {
|
|
|
1552
1788
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1553
1789
|
)
|
|
1554
1790
|
);
|
|
1555
|
-
const conn = await openConnection(cfg, deliver);
|
|
1791
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1556
1792
|
await reconcile(cfg, filter, deliver);
|
|
1557
1793
|
if (json) {
|
|
1558
1794
|
emit(
|
|
@@ -1601,7 +1837,7 @@ function registerChannel(program2) {
|
|
|
1601
1837
|
`))
|
|
1602
1838
|
);
|
|
1603
1839
|
});
|
|
1604
|
-
const conn = await openConnection(cfg, deliver);
|
|
1840
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1605
1841
|
await reconcile(cfg, filter, deliver);
|
|
1606
1842
|
process.stderr.write(
|
|
1607
1843
|
style.dim(
|
|
@@ -1712,8 +1948,9 @@ async function ensureSubscription(cfg, name, filter) {
|
|
|
1712
1948
|
);
|
|
1713
1949
|
return await resp.json();
|
|
1714
1950
|
}
|
|
1715
|
-
async function openConnection(cfg, onEvent) {
|
|
1716
|
-
const
|
|
1951
|
+
async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
1952
|
+
const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
|
|
1953
|
+
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
|
|
1717
1954
|
transport: HttpTransportType.LongPolling,
|
|
1718
1955
|
accessTokenFactory: () => requireToken(cfg)
|
|
1719
1956
|
}).withAutomaticReconnect().build();
|
|
@@ -2416,6 +2653,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2416
2653
|
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2417
2654
|
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2418
2655
|
`);
|
|
2656
|
+
if (dryRun) {
|
|
2657
|
+
for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
|
|
2658
|
+
process.stdout.write(` ${event}: ${command}
|
|
2659
|
+
`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2419
2662
|
for (const r of surfaceResults) {
|
|
2420
2663
|
results.push(r);
|
|
2421
2664
|
process.stdout.write(describe(r, dryRun) + "\n");
|
|
@@ -2533,6 +2776,178 @@ Examples:
|
|
|
2533
2776
|
});
|
|
2534
2777
|
}
|
|
2535
2778
|
|
|
2779
|
+
// src/commands/close.ts
|
|
2780
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2781
|
+
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2782
|
+
function registerClose(program2) {
|
|
2783
|
+
program2.command("close").description(
|
|
2784
|
+
"Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
|
|
2785
|
+
).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
|
|
2786
|
+
"--workspace <wsp>",
|
|
2787
|
+
"Workspace that owns the closeout memo"
|
|
2788
|
+
).requiredOption("--title <title>", "Closeout title").option(
|
|
2789
|
+
"--file <path>",
|
|
2790
|
+
"Read the closeout body from a file (default: stdin)"
|
|
2791
|
+
).option(
|
|
2792
|
+
"--decomposition <id>",
|
|
2793
|
+
"Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
|
|
2794
|
+
).option(
|
|
2795
|
+
"--no-status-flip",
|
|
2796
|
+
"Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
|
|
2797
|
+
).option(
|
|
2798
|
+
"--to-version <n>",
|
|
2799
|
+
"Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
|
|
2800
|
+
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2801
|
+
"after",
|
|
2802
|
+
`
|
|
2803
|
+
Examples:
|
|
2804
|
+
# Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
|
|
2805
|
+
$ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
|
|
2806
|
+
--verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
|
|
2807
|
+
|
|
2808
|
+
# Bare task:
|
|
2809
|
+
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
2810
|
+
--title "done" --file ./closeout.md`
|
|
2811
|
+
).action(async (opts, cmd) => {
|
|
2812
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2813
|
+
const json = cmd.optsWithGlobals().json;
|
|
2814
|
+
const verdict = String(opts.verdict);
|
|
2815
|
+
if (!VERDICTS.includes(verdict))
|
|
2816
|
+
fail(
|
|
2817
|
+
`--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
|
|
2818
|
+
);
|
|
2819
|
+
let bodyText;
|
|
2820
|
+
try {
|
|
2821
|
+
bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
|
|
2822
|
+
} catch {
|
|
2823
|
+
fail(
|
|
2824
|
+
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (!bodyText.trim())
|
|
2828
|
+
fail(
|
|
2829
|
+
"closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
|
|
2830
|
+
);
|
|
2831
|
+
const client = await makeClient(cfg);
|
|
2832
|
+
let matchTags;
|
|
2833
|
+
let dispatchedVersion;
|
|
2834
|
+
if (opts.decomposition) {
|
|
2835
|
+
const run = await runApi(
|
|
2836
|
+
"Reading run contract",
|
|
2837
|
+
async () => client.GET("/decompositions/{id}/run", {
|
|
2838
|
+
params: { path: { id: opts.decomposition } }
|
|
2839
|
+
})
|
|
2840
|
+
);
|
|
2841
|
+
const await_ = run.awaitingCompletion;
|
|
2842
|
+
if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
|
|
2843
|
+
fail(
|
|
2844
|
+
`decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
|
|
2845
|
+
);
|
|
2846
|
+
if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
|
|
2847
|
+
fail(
|
|
2848
|
+
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2849
|
+
);
|
|
2850
|
+
matchTags = await_.matchTags;
|
|
2851
|
+
if (typeof await_.dispatchedTaskVersion === "number")
|
|
2852
|
+
dispatchedVersion = await_.dispatchedTaskVersion;
|
|
2853
|
+
} else {
|
|
2854
|
+
matchTags = [`wlp-task:${opts.task}`];
|
|
2855
|
+
}
|
|
2856
|
+
const tags = [
|
|
2857
|
+
.../* @__PURE__ */ new Set([
|
|
2858
|
+
...matchTags,
|
|
2859
|
+
`wlp-task:${opts.task}`,
|
|
2860
|
+
`verdict:${verdict}`
|
|
2861
|
+
])
|
|
2862
|
+
];
|
|
2863
|
+
const closeout = await runApi(
|
|
2864
|
+
"Creating closeout",
|
|
2865
|
+
async () => client.POST("/memories", {
|
|
2866
|
+
body: {
|
|
2867
|
+
text: bodyText,
|
|
2868
|
+
type: "reference",
|
|
2869
|
+
content: "{}",
|
|
2870
|
+
confidence: 1,
|
|
2871
|
+
source: opts.source,
|
|
2872
|
+
archetype: "Document",
|
|
2873
|
+
title: opts.title,
|
|
2874
|
+
tags,
|
|
2875
|
+
owner: { type: "Workspace", id: opts.workspace }
|
|
2876
|
+
}
|
|
2877
|
+
})
|
|
2878
|
+
);
|
|
2879
|
+
let toVersion;
|
|
2880
|
+
if (opts.toVersion !== void 0) {
|
|
2881
|
+
toVersion = Number(opts.toVersion);
|
|
2882
|
+
if (!Number.isInteger(toVersion) || toVersion < 1)
|
|
2883
|
+
fail(
|
|
2884
|
+
`--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
|
|
2885
|
+
);
|
|
2886
|
+
} else if (dispatchedVersion !== void 0) {
|
|
2887
|
+
toVersion = dispatchedVersion;
|
|
2888
|
+
} else {
|
|
2889
|
+
const task = await runApi(
|
|
2890
|
+
"Reading task version",
|
|
2891
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2892
|
+
params: { path: { memoryId: opts.task } }
|
|
2893
|
+
})
|
|
2894
|
+
);
|
|
2895
|
+
const v = task?.item?.currentVersion ?? task?.currentVersion;
|
|
2896
|
+
if (typeof v !== "number" || v < 1)
|
|
2897
|
+
fail(
|
|
2898
|
+
`could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
|
|
2899
|
+
);
|
|
2900
|
+
toVersion = v;
|
|
2901
|
+
}
|
|
2902
|
+
const edge = await runApi(
|
|
2903
|
+
"Wiring Reference edge",
|
|
2904
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2905
|
+
params: { path: { memoryId: closeout.id } },
|
|
2906
|
+
body: { toMemoryId: opts.task, type: "Reference", toVersion }
|
|
2907
|
+
})
|
|
2908
|
+
);
|
|
2909
|
+
let taskStatus;
|
|
2910
|
+
if (opts.statusFlip !== false && !opts.decomposition) {
|
|
2911
|
+
const current = await runApi(
|
|
2912
|
+
"Reading task tags",
|
|
2913
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2914
|
+
params: { path: { memoryId: opts.task } }
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2917
|
+
const currentTags = current?.item?.tags ?? current?.tags ?? [];
|
|
2918
|
+
const target = verdict === "blocked" ? "status:blocked" : "status:done";
|
|
2919
|
+
const nextTags = [
|
|
2920
|
+
...new Set(currentTags.filter((t) => !t.startsWith("status:")))
|
|
2921
|
+
].concat(target);
|
|
2922
|
+
await runApi(
|
|
2923
|
+
"Flipping task status",
|
|
2924
|
+
async () => client.PATCH("/memories/{memoryId}/metadata", {
|
|
2925
|
+
params: { path: { memoryId: opts.task } },
|
|
2926
|
+
body: {
|
|
2927
|
+
memoryId: opts.task,
|
|
2928
|
+
source: opts.source,
|
|
2929
|
+
tags: nextTags
|
|
2930
|
+
}
|
|
2931
|
+
})
|
|
2932
|
+
);
|
|
2933
|
+
taskStatus = target.slice("status:".length);
|
|
2934
|
+
}
|
|
2935
|
+
const view = resolveViewUrl(cfg.baseUrl, closeout.url);
|
|
2936
|
+
const result = {
|
|
2937
|
+
closeoutId: closeout.id,
|
|
2938
|
+
edgeId: edge.id,
|
|
2939
|
+
taskStatus: taskStatus ?? null,
|
|
2940
|
+
verdict,
|
|
2941
|
+
tags
|
|
2942
|
+
};
|
|
2943
|
+
emitAction(
|
|
2944
|
+
`closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
|
|
2945
|
+
result,
|
|
2946
|
+
json
|
|
2947
|
+
);
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2536
2951
|
// src/commands/continuity.ts
|
|
2537
2952
|
function registerContinuity(program2) {
|
|
2538
2953
|
const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
|
|
@@ -2693,6 +3108,442 @@ Examples:
|
|
|
2693
3108
|
});
|
|
2694
3109
|
}
|
|
2695
3110
|
|
|
3111
|
+
// src/commands/decomposition.ts
|
|
3112
|
+
function registerDecomposition(program2) {
|
|
3113
|
+
const decomposition = program2.command("decomposition").description(
|
|
3114
|
+
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
3115
|
+
);
|
|
3116
|
+
decomposition.addHelpText(
|
|
3117
|
+
"after",
|
|
3118
|
+
`
|
|
3119
|
+
Examples:
|
|
3120
|
+
$ sechroom decomposition decompose mem_XXXX
|
|
3121
|
+
$ sechroom decomposition execute sug_XXXX
|
|
3122
|
+
$ sechroom decomposition publish-run sug_XXXX
|
|
3123
|
+
$ sechroom decomposition accept sug_XXXX
|
|
3124
|
+
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
3125
|
+
);
|
|
3126
|
+
decomposition.command("decompose <briefId>").description(
|
|
3127
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
3128
|
+
).action(async (briefId, _opts, cmd) => {
|
|
3129
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3130
|
+
const data = await runApi("Queueing decomposition", async () => {
|
|
3131
|
+
const client = await makeClient(cfg);
|
|
3132
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
3133
|
+
params: { path: { id: briefId } },
|
|
3134
|
+
body: { id: briefId }
|
|
3135
|
+
});
|
|
3136
|
+
});
|
|
3137
|
+
emitAction(
|
|
3138
|
+
`queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
|
|
3139
|
+
data,
|
|
3140
|
+
cmd.optsWithGlobals().json
|
|
3141
|
+
);
|
|
3142
|
+
});
|
|
3143
|
+
decomposition.command("execute <decompositionId>").description(
|
|
3144
|
+
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
3145
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3146
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3147
|
+
const data = await runApi("Executing decomposition", async () => {
|
|
3148
|
+
const client = await makeClient(cfg);
|
|
3149
|
+
return client.POST("/decompositions/{id}/execute", {
|
|
3150
|
+
params: { path: { id: decompositionId } },
|
|
3151
|
+
body: {}
|
|
3152
|
+
});
|
|
3153
|
+
});
|
|
3154
|
+
emitAction(
|
|
3155
|
+
`executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
3156
|
+
data,
|
|
3157
|
+
cmd.optsWithGlobals().json
|
|
3158
|
+
);
|
|
3159
|
+
});
|
|
3160
|
+
decomposition.command("publish-run <decompositionId>").description(
|
|
3161
|
+
"Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
|
|
3162
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3163
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3164
|
+
const data = await runApi("Publishing context pack", async () => {
|
|
3165
|
+
const client = await makeClient(cfg);
|
|
3166
|
+
return client.POST("/decompositions/{id}/publish-run", {
|
|
3167
|
+
params: { path: { id: decompositionId } },
|
|
3168
|
+
body: {}
|
|
3169
|
+
});
|
|
3170
|
+
});
|
|
3171
|
+
emitAction(
|
|
3172
|
+
`published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
3173
|
+
data,
|
|
3174
|
+
cmd.optsWithGlobals().json
|
|
3175
|
+
);
|
|
3176
|
+
});
|
|
3177
|
+
decomposition.command("accept <decompositionId>").description(
|
|
3178
|
+
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
3179
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3180
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3181
|
+
const data = await runApi("Accepting decomposition", async () => {
|
|
3182
|
+
const client = await makeClient(cfg);
|
|
3183
|
+
return client.POST("/decompositions/{id}/accept", {
|
|
3184
|
+
params: { path: { id: decompositionId } },
|
|
3185
|
+
body: {}
|
|
3186
|
+
});
|
|
3187
|
+
});
|
|
3188
|
+
emitAction(
|
|
3189
|
+
`accepted decomposition ${style.bold(decompositionId)}`,
|
|
3190
|
+
data,
|
|
3191
|
+
cmd.optsWithGlobals().json
|
|
3192
|
+
);
|
|
3193
|
+
});
|
|
3194
|
+
decomposition.command("reject <decompositionId>").description(
|
|
3195
|
+
"Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
|
|
3196
|
+
).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
|
|
3197
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3198
|
+
const data = await runApi("Rejecting decomposition", async () => {
|
|
3199
|
+
const client = await makeClient(cfg);
|
|
3200
|
+
return client.POST("/decompositions/{id}/reject", {
|
|
3201
|
+
params: { path: { id: decompositionId } },
|
|
3202
|
+
body: { reasonText: opts.reason ?? null }
|
|
3203
|
+
});
|
|
3204
|
+
});
|
|
3205
|
+
emitAction(
|
|
3206
|
+
`rejected decomposition ${style.bold(decompositionId)}`,
|
|
3207
|
+
data,
|
|
3208
|
+
cmd.optsWithGlobals().json
|
|
3209
|
+
);
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// src/commands/executor.ts
|
|
3214
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
3215
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3216
|
+
var EXECUTOR_STATE = "executor.json";
|
|
3217
|
+
var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
|
|
3218
|
+
var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
|
|
3219
|
+
var CLAUDE_EXECUTOR_HOOKS = {
|
|
3220
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3221
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3222
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3223
|
+
Stop: EXECUTOR_PULSE_COMMAND,
|
|
3224
|
+
SessionEnd: EXECUTOR_STOP_COMMAND
|
|
3225
|
+
};
|
|
3226
|
+
var CODEX_EXECUTOR_HOOKS = {
|
|
3227
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3228
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3229
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3230
|
+
Stop: EXECUTOR_PULSE_COMMAND
|
|
3231
|
+
};
|
|
3232
|
+
function registerExecutor(program2) {
|
|
3233
|
+
const executor = program2.command("executor").description(
|
|
3234
|
+
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
3235
|
+
);
|
|
3236
|
+
executor.command("install").description("Configure this checkout's harness to advertise itself as a WLP executor").option("--connector <id>", "Approved local-session ConnectorDefinition id").option("--instance-key <key>", "Stable executor identity (defaults to .sechroom/lane.json code-lane)").option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option("--capability <key...>", "Capability operation keys claimed by this instance").option("--relay <id>", "Relay identity shared by sibling instances", "sechroom-cli-local").option("--subscription-name <name>", "SignalR delivery binding name", "executor-dispatch").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option("--refresh-after <seconds>", "Minimum age before a hook refreshes", parseInteger, 40).option("-y, --yes", "Non-interactive: accept detected surface and lane defaults", false).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
|
|
3237
|
+
const globals = cmd.optsWithGlobals();
|
|
3238
|
+
const lane = readSem()?.values["code-lane"];
|
|
3239
|
+
const detected = detectHookSurfaces(process.cwd());
|
|
3240
|
+
let surface = opts.surface;
|
|
3241
|
+
let instanceKey = opts.instanceKey;
|
|
3242
|
+
let runtime = opts.runtime;
|
|
3243
|
+
let connector = opts.connector;
|
|
3244
|
+
let capabilities = opts.capability;
|
|
3245
|
+
const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
|
|
3246
|
+
if (!opts.yes && canPrompt()) {
|
|
3247
|
+
surface = await promptText("Harness surface (claude or codex)?", surface ?? surfaceDefault);
|
|
3248
|
+
instanceKey = await promptText("Executor instance key?", instanceKey ?? lane ?? "");
|
|
3249
|
+
runtime = await promptText("Runtime (claude-code or codex)?", runtime ?? (surface === "codex" ? "codex" : "claude-code"));
|
|
3250
|
+
connector = await promptText("Approved local-session connector id?", connector ?? "");
|
|
3251
|
+
const capabilityText = await promptText("Capability keys (comma-separated; blank for none)?", capabilities?.join(",") ?? "");
|
|
3252
|
+
capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
|
|
3253
|
+
}
|
|
3254
|
+
surface ??= surfaceDefault;
|
|
3255
|
+
instanceKey ??= lane;
|
|
3256
|
+
runtime ??= surface === "codex" ? "codex" : "claude-code";
|
|
3257
|
+
if (!connector) fail("executor install requires --connector (or an interactive connector id)");
|
|
3258
|
+
if (!instanceKey) fail("no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane");
|
|
3259
|
+
if (!opts.yes && !canPrompt()) fail("non-interactive executor install requires --yes");
|
|
3260
|
+
parseRuntimeKind(runtime);
|
|
3261
|
+
if (!["claude", "codex"].includes(surface)) fail("surface must be claude or codex");
|
|
3262
|
+
if (opts.refreshAfter >= opts.ttl) fail("refresh-after must be shorter than the TTL");
|
|
3263
|
+
const sem = readSem();
|
|
3264
|
+
const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
|
|
3265
|
+
const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
|
|
3266
|
+
const state = {
|
|
3267
|
+
schemaVersion: 1,
|
|
3268
|
+
instanceKey,
|
|
3269
|
+
runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
|
|
3270
|
+
connectorId: connector,
|
|
3271
|
+
capabilityKeys: capabilities ?? [],
|
|
3272
|
+
relayId: opts.relay,
|
|
3273
|
+
subscriptionName: opts.subscriptionName,
|
|
3274
|
+
ttlSeconds: opts.ttl,
|
|
3275
|
+
refreshAfterSeconds: opts.refreshAfter
|
|
3276
|
+
};
|
|
3277
|
+
if (!opts.dryRun) {
|
|
3278
|
+
mkdirSync9(dirname8(statePath), { recursive: true });
|
|
3279
|
+
writeFileSync9(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
3280
|
+
ensureStateDirIgnored(checkout);
|
|
3281
|
+
}
|
|
3282
|
+
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map((target) => target.dir) : [join11(checkout, ".claude")];
|
|
3283
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
|
|
3284
|
+
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
3285
|
+
for (const target of hookTargets) {
|
|
3286
|
+
const results = surface === "claude" ? [installClaudeCommands(target, CLAUDE_EXECUTOR_HOOKS, opts.dryRun)] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
|
|
3287
|
+
for (const result of results) process.stderr.write(describe(result, opts.dryRun) + "\n");
|
|
3288
|
+
}
|
|
3289
|
+
warnIfSechroomNotOnPath();
|
|
3290
|
+
process.stderr.write(style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
|
|
3291
|
+
`));
|
|
3292
|
+
});
|
|
3293
|
+
executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
|
|
3294
|
+
await drainStdin();
|
|
3295
|
+
const located = readExecutorState();
|
|
3296
|
+
if (!located) return;
|
|
3297
|
+
const { state, path } = located;
|
|
3298
|
+
const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
|
|
3299
|
+
if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
|
|
3300
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3301
|
+
try {
|
|
3302
|
+
let data;
|
|
3303
|
+
if (state.instanceId) {
|
|
3304
|
+
try {
|
|
3305
|
+
data = await refresh(cfg, state.instanceId, state.ttlSeconds);
|
|
3306
|
+
} catch {
|
|
3307
|
+
data = await registerInstance(cfg, state);
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
data = await registerInstance(cfg, state);
|
|
3311
|
+
}
|
|
3312
|
+
state.instanceId = data.id;
|
|
3313
|
+
state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3314
|
+
writeFileSync9(path, JSON.stringify(state, null, 2) + "\n");
|
|
3315
|
+
} catch {
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
|
|
3319
|
+
await drainStdin();
|
|
3320
|
+
const located = readExecutorState();
|
|
3321
|
+
if (!located?.state.instanceId) return;
|
|
3322
|
+
try {
|
|
3323
|
+
await api(resolveConfig(cmd.optsWithGlobals()), `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`, { method: "DELETE", body: JSON.stringify({}) });
|
|
3324
|
+
delete located.state.instanceId;
|
|
3325
|
+
delete located.state.lastRefreshAt;
|
|
3326
|
+
writeFileSync9(located.path, JSON.stringify(located.state, null, 2) + "\n");
|
|
3327
|
+
} catch {
|
|
3328
|
+
}
|
|
3329
|
+
});
|
|
3330
|
+
executor.command("submit-connector").description(
|
|
3331
|
+
"Submit a local-session connector definition for governed approval"
|
|
3332
|
+
).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
|
|
3333
|
+
"base",
|
|
3334
|
+
"dotnet-10"
|
|
3335
|
+
]).action(async (opts, cmd) => {
|
|
3336
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3337
|
+
const data = await api(cfg, "/connectors/definitions", {
|
|
3338
|
+
method: "POST",
|
|
3339
|
+
body: JSON.stringify({
|
|
3340
|
+
slug: opts.slug,
|
|
3341
|
+
displayName: opts.displayName,
|
|
3342
|
+
runtimeProfiles: opts.profile,
|
|
3343
|
+
connectorKind: "ExecutionRuntime",
|
|
3344
|
+
providerKind: "local-session",
|
|
3345
|
+
dispatchTransport: parseTransport(opts.transport)
|
|
3346
|
+
})
|
|
3347
|
+
});
|
|
3348
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3349
|
+
if (!cmd.optsWithGlobals().json) {
|
|
3350
|
+
process.stderr.write(
|
|
3351
|
+
style.dim(
|
|
3352
|
+
"approve this connector definition before registering executors\n"
|
|
3353
|
+
)
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
executor.command("register").description(
|
|
3358
|
+
"Create/reuse a SignalR binding and register this local executor instance"
|
|
3359
|
+
).requiredOption(
|
|
3360
|
+
"--instance-key <key>",
|
|
3361
|
+
"Stable key for this concrete session/lane"
|
|
3362
|
+
).requiredOption(
|
|
3363
|
+
"--connector <id>",
|
|
3364
|
+
"Approved local-session ConnectorDefinition id"
|
|
3365
|
+
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
3366
|
+
"--relay <id>",
|
|
3367
|
+
"Relay identity shared by sibling instances",
|
|
3368
|
+
"sechroom-cli-local"
|
|
3369
|
+
).option(
|
|
3370
|
+
"--subscription-name <name>",
|
|
3371
|
+
"SignalR delivery binding name",
|
|
3372
|
+
"executor-dispatch"
|
|
3373
|
+
).option(
|
|
3374
|
+
"--capability <key...>",
|
|
3375
|
+
"Capability operation keys claimed by this instance"
|
|
3376
|
+
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
3377
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3378
|
+
if (!cfg.workspaceId)
|
|
3379
|
+
fail("executor register requires a configured workspaceId");
|
|
3380
|
+
const subscription = await api(
|
|
3381
|
+
cfg,
|
|
3382
|
+
"/me/delivery-subscriptions/signalr",
|
|
3383
|
+
{
|
|
3384
|
+
method: "POST",
|
|
3385
|
+
body: JSON.stringify({
|
|
3386
|
+
name: opts.subscriptionName,
|
|
3387
|
+
enabled: true,
|
|
3388
|
+
filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
|
|
3389
|
+
})
|
|
3390
|
+
}
|
|
3391
|
+
);
|
|
3392
|
+
const data = await api(
|
|
3393
|
+
cfg,
|
|
3394
|
+
"/me/executor-instances",
|
|
3395
|
+
{
|
|
3396
|
+
method: "POST",
|
|
3397
|
+
body: JSON.stringify({
|
|
3398
|
+
relayId: opts.relay,
|
|
3399
|
+
instanceKey: opts.instanceKey,
|
|
3400
|
+
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
3401
|
+
activationMode: "Attached",
|
|
3402
|
+
deliverySubscriptionId: subscription.id,
|
|
3403
|
+
connectorId: opts.connector,
|
|
3404
|
+
claimedCapabilityKeys: opts.capability ?? [],
|
|
3405
|
+
toolSetRef: opts.toolSetRef ?? null,
|
|
3406
|
+
ttlSeconds: opts.ttl
|
|
3407
|
+
})
|
|
3408
|
+
}
|
|
3409
|
+
);
|
|
3410
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3411
|
+
if (!cmd.optsWithGlobals().json)
|
|
3412
|
+
process.stderr.write(
|
|
3413
|
+
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
3414
|
+
`)
|
|
3415
|
+
);
|
|
3416
|
+
});
|
|
3417
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
3418
|
+
const data = await refresh(
|
|
3419
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
3420
|
+
id,
|
|
3421
|
+
opts.ttl
|
|
3422
|
+
);
|
|
3423
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3424
|
+
});
|
|
3425
|
+
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) => {
|
|
3426
|
+
if (opts.interval >= opts.ttl)
|
|
3427
|
+
fail("heartbeat interval must be shorter than the TTL");
|
|
3428
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3429
|
+
await refresh(cfg, id, opts.ttl);
|
|
3430
|
+
process.stderr.write(
|
|
3431
|
+
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
3432
|
+
`)
|
|
3433
|
+
);
|
|
3434
|
+
await holdHeartbeat(async () => {
|
|
3435
|
+
await refresh(cfg, id, opts.ttl);
|
|
3436
|
+
}, opts.interval * 1e3);
|
|
3437
|
+
});
|
|
3438
|
+
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
3439
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3440
|
+
const data = await api(
|
|
3441
|
+
cfg,
|
|
3442
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
3443
|
+
);
|
|
3444
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3445
|
+
});
|
|
3446
|
+
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
3447
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3448
|
+
const data = await api(
|
|
3449
|
+
cfg,
|
|
3450
|
+
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
3451
|
+
{
|
|
3452
|
+
method: "DELETE",
|
|
3453
|
+
body: JSON.stringify({})
|
|
3454
|
+
}
|
|
3455
|
+
);
|
|
3456
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
function parseRuntimeKind(value) {
|
|
3460
|
+
switch (value.trim().toLowerCase()) {
|
|
3461
|
+
case "claude":
|
|
3462
|
+
case "claude-code":
|
|
3463
|
+
return "ClaudeCode";
|
|
3464
|
+
case "codex":
|
|
3465
|
+
return "Codex";
|
|
3466
|
+
default:
|
|
3467
|
+
return fail("runtime must be claude-code or codex");
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function parseTransport(value) {
|
|
3471
|
+
switch (value.trim().toLowerCase()) {
|
|
3472
|
+
case "push":
|
|
3473
|
+
return "Push";
|
|
3474
|
+
case "pull":
|
|
3475
|
+
return "Pull";
|
|
3476
|
+
default:
|
|
3477
|
+
return fail("transport must be push or pull");
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
async function refresh(cfg, id, ttlSeconds) {
|
|
3481
|
+
return api(
|
|
3482
|
+
cfg,
|
|
3483
|
+
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
3484
|
+
{
|
|
3485
|
+
method: "POST",
|
|
3486
|
+
body: JSON.stringify({ ttlSeconds })
|
|
3487
|
+
}
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
async function registerInstance(cfg, state) {
|
|
3491
|
+
if (!cfg.workspaceId) fail("executor registration requires a configured workspaceId");
|
|
3492
|
+
const subscription = await api(cfg, "/me/delivery-subscriptions/signalr", {
|
|
3493
|
+
method: "POST",
|
|
3494
|
+
body: JSON.stringify({ name: state.subscriptionName, enabled: true, filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] } })
|
|
3495
|
+
});
|
|
3496
|
+
return api(cfg, "/me/executor-instances", {
|
|
3497
|
+
method: "POST",
|
|
3498
|
+
body: JSON.stringify({ relayId: state.relayId, instanceKey: state.instanceKey, runtimeKind: parseRuntimeKind(state.runtime), activationMode: "Attached", deliverySubscriptionId: subscription.id, connectorId: state.connectorId, claimedCapabilityKeys: state.capabilityKeys, toolSetRef: null, ttlSeconds: state.ttlSeconds })
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
function readExecutorState(start = process.cwd()) {
|
|
3502
|
+
const semPath = resolveSemPathForRead(start);
|
|
3503
|
+
const sem = semPath ? readSem(semPath) : void 0;
|
|
3504
|
+
const path = join11(sem ? dirname8(sem.path) : join11(start, ".sechroom"), EXECUTOR_STATE);
|
|
3505
|
+
if (!existsSync9(path)) return void 0;
|
|
3506
|
+
return { state: JSON.parse(readFileSync8(path, "utf8")), path };
|
|
3507
|
+
}
|
|
3508
|
+
async function drainStdin() {
|
|
3509
|
+
if (process.stdin.isTTY) return;
|
|
3510
|
+
for await (const _chunk of process.stdin) {
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
async function api(cfg, path, init) {
|
|
3514
|
+
const token = await requireToken(cfg);
|
|
3515
|
+
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
3516
|
+
...init,
|
|
3517
|
+
headers: {
|
|
3518
|
+
authorization: `Bearer ${token}`,
|
|
3519
|
+
tenant: cfg.tenant,
|
|
3520
|
+
"content-type": "application/json",
|
|
3521
|
+
"x-sechroom-surface": "cli"
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
if (!response.ok)
|
|
3525
|
+
fail(
|
|
3526
|
+
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
3527
|
+
);
|
|
3528
|
+
return response.json();
|
|
3529
|
+
}
|
|
3530
|
+
function parseInteger(value) {
|
|
3531
|
+
const parsed = Number.parseInt(value, 10);
|
|
3532
|
+
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
3533
|
+
return parsed;
|
|
3534
|
+
}
|
|
3535
|
+
function holdHeartbeat(tick, intervalMs) {
|
|
3536
|
+
return new Promise((resolve3, reject) => {
|
|
3537
|
+
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
3538
|
+
const stop = () => {
|
|
3539
|
+
clearInterval(timer);
|
|
3540
|
+
resolve3();
|
|
3541
|
+
};
|
|
3542
|
+
process.once("SIGINT", stop);
|
|
3543
|
+
process.once("SIGTERM", stop);
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
|
|
2696
3547
|
// src/commands/filing.ts
|
|
2697
3548
|
function registerFiling(program2) {
|
|
2698
3549
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3204,8 +4055,8 @@ Examples:
|
|
|
3204
4055
|
|
|
3205
4056
|
// src/setup/apply.ts
|
|
3206
4057
|
import { createHash as createHash3 } from "crypto";
|
|
3207
|
-
import { mkdirSync as
|
|
3208
|
-
import { dirname as
|
|
4058
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
|
|
4059
|
+
import { dirname as dirname9 } from "path";
|
|
3209
4060
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3210
4061
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3211
4062
|
function normalizeBody(s) {
|
|
@@ -3258,22 +4109,22 @@ function parseManagedBlock(content, block) {
|
|
|
3258
4109
|
return null;
|
|
3259
4110
|
}
|
|
3260
4111
|
function ensureDir2(path) {
|
|
3261
|
-
|
|
4112
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3262
4113
|
}
|
|
3263
4114
|
function readOr(path, fallback) {
|
|
3264
4115
|
try {
|
|
3265
|
-
return
|
|
4116
|
+
return readFileSync9(path, "utf8");
|
|
3266
4117
|
} catch {
|
|
3267
4118
|
return fallback;
|
|
3268
4119
|
}
|
|
3269
4120
|
}
|
|
3270
4121
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3271
4122
|
const incoming = JSON.parse(snippet);
|
|
3272
|
-
const existed =
|
|
4123
|
+
const existed = existsSync10(path);
|
|
3273
4124
|
let current = {};
|
|
3274
4125
|
if (existed) {
|
|
3275
4126
|
try {
|
|
3276
|
-
current = JSON.parse(
|
|
4127
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3277
4128
|
} catch {
|
|
3278
4129
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3279
4130
|
}
|
|
@@ -3281,26 +4132,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3281
4132
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3282
4133
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3283
4134
|
ensureDir2(path);
|
|
3284
|
-
|
|
4135
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3285
4136
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3286
4137
|
}
|
|
3287
4138
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3288
|
-
const existed =
|
|
4139
|
+
const existed = existsSync10(path);
|
|
3289
4140
|
let body = readOr(path, "");
|
|
3290
4141
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3291
4142
|
const trimmed = body.trim();
|
|
3292
4143
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3293
4144
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3294
4145
|
ensureDir2(path);
|
|
3295
|
-
|
|
4146
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3296
4147
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3297
4148
|
}
|
|
3298
4149
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3299
|
-
const existed =
|
|
4150
|
+
const existed = existsSync10(path);
|
|
3300
4151
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3301
4152
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3302
4153
|
ensureDir2(path);
|
|
3303
|
-
|
|
4154
|
+
writeFileSync10(path, next);
|
|
3304
4155
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3305
4156
|
}
|
|
3306
4157
|
function computeBlockFile(current, write) {
|
|
@@ -3341,7 +4192,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3341
4192
|
const next = computeBlockFile(current, write);
|
|
3342
4193
|
if (!dryRun) {
|
|
3343
4194
|
ensureDir2(proposedPath);
|
|
3344
|
-
|
|
4195
|
+
writeFileSync10(proposedPath, next);
|
|
3345
4196
|
}
|
|
3346
4197
|
return {
|
|
3347
4198
|
kind: "instruction",
|
|
@@ -3471,8 +4322,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3471
4322
|
}
|
|
3472
4323
|
|
|
3473
4324
|
// src/setup/skills-offer.ts
|
|
3474
|
-
import { mkdirSync as
|
|
3475
|
-
import { join as
|
|
4325
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4326
|
+
import { join as join12 } from "path";
|
|
3476
4327
|
|
|
3477
4328
|
// src/setup/lane-pin.ts
|
|
3478
4329
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3588,8 +4439,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3588
4439
|
if (skills.length > 0) {
|
|
3589
4440
|
const written = [];
|
|
3590
4441
|
for (const s of skills) {
|
|
3591
|
-
|
|
3592
|
-
|
|
4442
|
+
mkdirSync11(join12(sDir, s.name), { recursive: true });
|
|
4443
|
+
writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3593
4444
|
written.push(s.name);
|
|
3594
4445
|
}
|
|
3595
4446
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3597,11 +4448,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3597
4448
|
`);
|
|
3598
4449
|
}
|
|
3599
4450
|
if (agents.length > 0) {
|
|
3600
|
-
|
|
4451
|
+
mkdirSync11(aDir, { recursive: true });
|
|
3601
4452
|
const written = [];
|
|
3602
4453
|
for (const a of agents) {
|
|
3603
4454
|
const file = `${a.name}.md`;
|
|
3604
|
-
|
|
4455
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3605
4456
|
written.push(file);
|
|
3606
4457
|
}
|
|
3607
4458
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3984,13 +4835,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
3984
4835
|
}
|
|
3985
4836
|
|
|
3986
4837
|
// src/commands/onboard.ts
|
|
3987
|
-
import { existsSync as
|
|
3988
|
-
import { basename as basename2, join as
|
|
4838
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4839
|
+
import { basename as basename2, join as join14 } from "path";
|
|
3989
4840
|
|
|
3990
4841
|
// src/commands/fanout.ts
|
|
3991
4842
|
import { spawnSync } from "child_process";
|
|
3992
|
-
import { existsSync as
|
|
3993
|
-
import { isAbsolute, join as
|
|
4843
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4844
|
+
import { isAbsolute, join as join13, resolve } from "path";
|
|
3994
4845
|
var ICON = {
|
|
3995
4846
|
refresh: "\u21BB",
|
|
3996
4847
|
bind: "+",
|
|
@@ -4010,21 +4861,21 @@ function discoverChildren(root) {
|
|
|
4010
4861
|
const out = [];
|
|
4011
4862
|
for (const name of names.sort()) {
|
|
4012
4863
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4013
|
-
const dir =
|
|
4864
|
+
const dir = join13(root, name);
|
|
4014
4865
|
try {
|
|
4015
4866
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4016
4867
|
} catch {
|
|
4017
4868
|
continue;
|
|
4018
4869
|
}
|
|
4019
|
-
if (
|
|
4870
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4020
4871
|
}
|
|
4021
4872
|
return out;
|
|
4022
4873
|
}
|
|
4023
4874
|
function readManifest(path) {
|
|
4024
|
-
if (!
|
|
4875
|
+
if (!existsSync11(path)) return null;
|
|
4025
4876
|
let parsed;
|
|
4026
4877
|
try {
|
|
4027
|
-
parsed = JSON.parse(
|
|
4878
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4028
4879
|
} catch (err2) {
|
|
4029
4880
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4030
4881
|
}
|
|
@@ -4390,10 +5241,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4390
5241
|
}
|
|
4391
5242
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4392
5243
|
const dir = resolveChildDir(entry.path, root);
|
|
4393
|
-
if (!
|
|
5244
|
+
if (!existsSync12(dir)) {
|
|
4394
5245
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4395
5246
|
}
|
|
4396
|
-
if (
|
|
5247
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
4397
5248
|
return {
|
|
4398
5249
|
label: entry.path,
|
|
4399
5250
|
dir,
|
|
@@ -4466,7 +5317,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4466
5317
|
async function runRecurse(cfg, g, opts) {
|
|
4467
5318
|
const { yes, dryRun, json } = opts;
|
|
4468
5319
|
const root = process.cwd();
|
|
4469
|
-
const manifestPath =
|
|
5320
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
4470
5321
|
const fromManifest = readManifest(manifestPath);
|
|
4471
5322
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4472
5323
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -4838,24 +5689,45 @@ Examples:
|
|
|
4838
5689
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
4839
5690
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
4840
5691
|
);
|
|
4841
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").
|
|
5692
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
|
|
5693
|
+
"--to-version <number>",
|
|
5694
|
+
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
5695
|
+
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
4842
5696
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
5697
|
+
const client = await makeClient(cfg);
|
|
5698
|
+
let toVersion;
|
|
5699
|
+
if (opts.toVersion !== void 0) {
|
|
5700
|
+
toVersion = Number(opts.toVersion);
|
|
5701
|
+
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
5702
|
+
fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
|
|
5703
|
+
}
|
|
5704
|
+
} else {
|
|
5705
|
+
const target = await runApi(
|
|
5706
|
+
"Resolving target version",
|
|
5707
|
+
async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
|
|
5708
|
+
);
|
|
5709
|
+
if (typeof target.item?.currentVersion !== "number") {
|
|
5710
|
+
fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
|
|
5711
|
+
}
|
|
5712
|
+
toVersion = target.item.currentVersion;
|
|
5713
|
+
}
|
|
5714
|
+
const data = await runApi(
|
|
5715
|
+
"Creating relationship",
|
|
5716
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
4846
5717
|
params: { path: { memoryId: fromMemoryId } },
|
|
4847
5718
|
body: {
|
|
4848
5719
|
toMemoryId,
|
|
4849
|
-
type: opts.type
|
|
5720
|
+
type: opts.type,
|
|
5721
|
+
toVersion
|
|
4850
5722
|
}
|
|
4851
|
-
})
|
|
4852
|
-
|
|
5723
|
+
})
|
|
5724
|
+
);
|
|
4853
5725
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
4854
5726
|
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
4855
5727
|
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
4856
5728
|
emitAction(
|
|
4857
|
-
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
4858
|
-
data,
|
|
5729
|
+
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
|
|
5730
|
+
{ ...data, toVersion },
|
|
4859
5731
|
cmd.optsWithGlobals().json
|
|
4860
5732
|
);
|
|
4861
5733
|
});
|
|
@@ -4978,23 +5850,23 @@ Examples:
|
|
|
4978
5850
|
|
|
4979
5851
|
// src/commands/reset.ts
|
|
4980
5852
|
import { homedir as homedir4 } from "os";
|
|
4981
|
-
import { join as
|
|
4982
|
-
import { existsSync as
|
|
5853
|
+
import { join as join15 } from "path";
|
|
5854
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
4983
5855
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4984
|
-
var localSkillsDir = () =>
|
|
4985
|
-
var globalSkillsDir = () =>
|
|
4986
|
-
var localAgentsDir = () =>
|
|
4987
|
-
var globalAgentsDir = () =>
|
|
5856
|
+
var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
|
|
5857
|
+
var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
|
|
5858
|
+
var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
|
|
5859
|
+
var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
|
|
4988
5860
|
function removeMaterialisedSkills(dir) {
|
|
4989
5861
|
const removed = [];
|
|
4990
|
-
const lockPath =
|
|
4991
|
-
if (!
|
|
5862
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5863
|
+
if (!existsSync13(lockPath)) return removed;
|
|
4992
5864
|
try {
|
|
4993
|
-
const lock = JSON.parse(
|
|
5865
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
4994
5866
|
for (const entry of Object.values(lock)) {
|
|
4995
5867
|
for (const name of entry.skills ?? []) {
|
|
4996
|
-
const p =
|
|
4997
|
-
if (
|
|
5868
|
+
const p = join15(dir, name);
|
|
5869
|
+
if (existsSync13(p)) {
|
|
4998
5870
|
rmSync3(p, { recursive: true, force: true });
|
|
4999
5871
|
removed.push(p);
|
|
5000
5872
|
}
|
|
@@ -5039,18 +5911,18 @@ function registerReset(program2) {
|
|
|
5039
5911
|
}
|
|
5040
5912
|
}
|
|
5041
5913
|
const removed = [];
|
|
5042
|
-
const stateDir =
|
|
5043
|
-
if (
|
|
5914
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
5915
|
+
if (existsSync13(stateDir)) {
|
|
5044
5916
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5045
5917
|
removed.push(stateDir);
|
|
5046
5918
|
}
|
|
5047
|
-
const legacyCfg =
|
|
5048
|
-
if (
|
|
5919
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
5920
|
+
if (existsSync13(legacyCfg)) {
|
|
5049
5921
|
rmSync3(legacyCfg, { force: true });
|
|
5050
5922
|
removed.push(legacyCfg);
|
|
5051
5923
|
}
|
|
5052
|
-
const legacySem =
|
|
5053
|
-
if (
|
|
5924
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
5925
|
+
if (existsSync13(legacySem)) {
|
|
5054
5926
|
rmSync3(legacySem, { force: true });
|
|
5055
5927
|
removed.push(legacySem);
|
|
5056
5928
|
}
|
|
@@ -5076,8 +5948,8 @@ function registerReset(program2) {
|
|
|
5076
5948
|
}
|
|
5077
5949
|
|
|
5078
5950
|
// src/commands/skills.ts
|
|
5079
|
-
import { existsSync as
|
|
5080
|
-
import { join as
|
|
5951
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
5952
|
+
import { join as join16 } from "path";
|
|
5081
5953
|
function filenameFromDisposition(header) {
|
|
5082
5954
|
if (!header) return void 0;
|
|
5083
5955
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5085,11 +5957,11 @@ function filenameFromDisposition(header) {
|
|
|
5085
5957
|
}
|
|
5086
5958
|
function resolveOutputPath(output, serverFilename) {
|
|
5087
5959
|
const filename = serverFilename || "skills.zip";
|
|
5088
|
-
if (!output) return
|
|
5089
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
5960
|
+
if (!output) return join16(process.cwd(), filename);
|
|
5961
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5090
5962
|
if (looksLikeDir) {
|
|
5091
|
-
|
|
5092
|
-
return
|
|
5963
|
+
mkdirSync12(output, { recursive: true });
|
|
5964
|
+
return join16(output, filename);
|
|
5093
5965
|
}
|
|
5094
5966
|
return output;
|
|
5095
5967
|
}
|
|
@@ -5120,7 +5992,7 @@ async function downloadZip(label, call, output) {
|
|
|
5120
5992
|
const buf = Buffer.from(res.data);
|
|
5121
5993
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5122
5994
|
const path = resolveOutputPath(output, filename);
|
|
5123
|
-
|
|
5995
|
+
writeFileSync12(path, buf);
|
|
5124
5996
|
return { path, bytes: buf.length, filename };
|
|
5125
5997
|
}
|
|
5126
5998
|
function registerSkills(program2) {
|
|
@@ -5129,7 +6001,9 @@ function registerSkills(program2) {
|
|
|
5129
6001
|
"after",
|
|
5130
6002
|
`
|
|
5131
6003
|
Examples:
|
|
5132
|
-
$ sechroom skills install
|
|
6004
|
+
$ sechroom skills install --client claude materialise target:claude-code skills to ~/.claude/skills
|
|
6005
|
+
$ sechroom skills install --client codex materialise target:gpt-codex skills to ~/.codex/skills
|
|
6006
|
+
$ sechroom skills install --client all keep both global clients in sync
|
|
5133
6007
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
5134
6008
|
$ sechroom skills list what's materialised on disk
|
|
5135
6009
|
$ sechroom skills clean remove the materialised skill files
|
|
@@ -5138,11 +6012,15 @@ Examples:
|
|
|
5138
6012
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
5139
6013
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
5140
6014
|
|
|
6015
|
+
When --client is omitted, configured client flags/environment variables win;
|
|
6016
|
+
otherwise existing default client homes are detected. Both configured/detected
|
|
6017
|
+
clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
6018
|
+
|
|
5141
6019
|
`
|
|
5142
6020
|
);
|
|
5143
|
-
skills.command("install").description("Materialise your installed skills 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(SKILL_SPEC, cmd, opts));
|
|
5144
|
-
skills.command("list").description("List the skills 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(SKILL_SPEC, cmd, opts));
|
|
5145
|
-
skills.command("clean [slug]").description(`Remove skill 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(SKILL_SPEC, cmd, opts, slugArg));
|
|
6021
|
+
skills.command("install").description("Materialise your installed skills 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("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
6022
|
+
skills.command("list").description("List the skills 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("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
6023
|
+
skills.command("clean [slug]").description(`Remove skill 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("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
5146
6024
|
skills.command("preview").description("Render a workspace's draft bundle from source (no publish/install) and report the components").requiredOption("--workspace <id>", "workspace holding the draft bundle sources (wsp_\u2026)").option("--slug <slug>", "override the derived bundle slug").option("--title <title>", "override the derived bundle title").option("--version <version>", "override the derived bundle version").option("--default-install-parent <path>", "override the derived default install parent").option("--json", "machine output (the full RenderBundlePreviewResponse)").action(async (opts, cmd) => {
|
|
5147
6025
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
5148
6026
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -5288,12 +6166,12 @@ Examples:
|
|
|
5288
6166
|
}
|
|
5289
6167
|
|
|
5290
6168
|
// src/commands/sweep.ts
|
|
5291
|
-
import { existsSync as
|
|
5292
|
-
import { dirname as
|
|
5293
|
-
var DEFAULT_MANIFEST =
|
|
6169
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6170
|
+
import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
|
|
6171
|
+
var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
|
|
5294
6172
|
function planEntry(entry, root) {
|
|
5295
6173
|
const dir = resolveChildDir(entry.path, root);
|
|
5296
|
-
if (!
|
|
6174
|
+
if (!existsSync15(dir)) {
|
|
5297
6175
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5298
6176
|
}
|
|
5299
6177
|
if (committedBindingPath(dir)) {
|
|
@@ -5369,7 +6247,7 @@ Examples:
|
|
|
5369
6247
|
`);
|
|
5370
6248
|
return;
|
|
5371
6249
|
}
|
|
5372
|
-
const root =
|
|
6250
|
+
const root = dirname10(dirname10(manifestPath));
|
|
5373
6251
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5374
6252
|
if (!json) {
|
|
5375
6253
|
process.stderr.write(
|
|
@@ -5388,13 +6266,13 @@ Examples:
|
|
|
5388
6266
|
|
|
5389
6267
|
// src/commands/telemetry.ts
|
|
5390
6268
|
import {
|
|
5391
|
-
existsSync as
|
|
5392
|
-
mkdirSync as
|
|
5393
|
-
readFileSync as
|
|
6269
|
+
existsSync as existsSync16,
|
|
6270
|
+
mkdirSync as mkdirSync13,
|
|
6271
|
+
readFileSync as readFileSync12,
|
|
5394
6272
|
rmSync as rmSync4,
|
|
5395
|
-
writeFileSync as
|
|
6273
|
+
writeFileSync as writeFileSync13
|
|
5396
6274
|
} from "fs";
|
|
5397
|
-
import { dirname as
|
|
6275
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
5398
6276
|
function registerTelemetry(program2) {
|
|
5399
6277
|
const telemetry = program2.command("telemetry").description(
|
|
5400
6278
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5464,14 +6342,14 @@ function registerTelemetry(program2) {
|
|
|
5464
6342
|
"Decomposition id this session executes"
|
|
5465
6343
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5466
6344
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5467
|
-
const dir =
|
|
5468
|
-
|
|
5469
|
-
const path =
|
|
6345
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6346
|
+
mkdirSync13(dir, { recursive: true });
|
|
6347
|
+
const path = join18(dir, BINDING_FILE);
|
|
5470
6348
|
const binding = {
|
|
5471
6349
|
decompositionId: opts.decomposition,
|
|
5472
6350
|
taskId: opts.task
|
|
5473
6351
|
};
|
|
5474
|
-
|
|
6352
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5475
6353
|
ensureStateDirIgnored(process.cwd());
|
|
5476
6354
|
if (json) {
|
|
5477
6355
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5486,8 +6364,8 @@ function registerTelemetry(program2) {
|
|
|
5486
6364
|
});
|
|
5487
6365
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5488
6366
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5489
|
-
const path =
|
|
5490
|
-
const existed =
|
|
6367
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6368
|
+
const existed = existsSync16(path);
|
|
5491
6369
|
if (existed) rmSync4(path);
|
|
5492
6370
|
if (json) emit({ unbound: existed, path }, true);
|
|
5493
6371
|
else
|
|
@@ -5496,7 +6374,7 @@ function registerTelemetry(program2) {
|
|
|
5496
6374
|
);
|
|
5497
6375
|
});
|
|
5498
6376
|
telemetry.command("hook").description(
|
|
5499
|
-
"Per-turn telemetry self-report for
|
|
6377
|
+
"Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
|
|
5500
6378
|
).action(async (_opts, cmd) => {
|
|
5501
6379
|
try {
|
|
5502
6380
|
const raw = await readStdin2();
|
|
@@ -5505,21 +6383,10 @@ function registerTelemetry(program2) {
|
|
|
5505
6383
|
const binding = findBinding(cwd);
|
|
5506
6384
|
if (!binding) return process.exit(0);
|
|
5507
6385
|
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5508
|
-
|
|
6386
|
+
const events = buildHookEvents(input, usage, binding.taskId);
|
|
6387
|
+
if (events.length === 0) return process.exit(0);
|
|
5509
6388
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5510
|
-
await postTelemetry(cfg, binding.decompositionId,
|
|
5511
|
-
{
|
|
5512
|
-
taskId: binding.taskId,
|
|
5513
|
-
kind: "Parsed",
|
|
5514
|
-
tokensIn: usage.tokensIn,
|
|
5515
|
-
tokensOut: usage.tokensOut,
|
|
5516
|
-
contextUsed: usage.contextUsed,
|
|
5517
|
-
contextWindow: usage.contextWindow,
|
|
5518
|
-
text: null,
|
|
5519
|
-
approvalState: null,
|
|
5520
|
-
verdict: null
|
|
5521
|
-
}
|
|
5522
|
-
]);
|
|
6389
|
+
await postTelemetry(cfg, binding.decompositionId, events);
|
|
5523
6390
|
return process.exit(0);
|
|
5524
6391
|
} catch {
|
|
5525
6392
|
return process.exit(0);
|
|
@@ -5547,7 +6414,12 @@ function registerTelemetry(program2) {
|
|
|
5547
6414
|
scope,
|
|
5548
6415
|
cwd
|
|
5549
6416
|
});
|
|
5550
|
-
const commands = {
|
|
6417
|
+
const commands = {
|
|
6418
|
+
Stop: "sechroom telemetry hook",
|
|
6419
|
+
SubagentStop: "sechroom telemetry hook",
|
|
6420
|
+
Notification: "sechroom telemetry hook",
|
|
6421
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
6422
|
+
};
|
|
5551
6423
|
try {
|
|
5552
6424
|
const multi = targets.length > 1;
|
|
5553
6425
|
const results = targets.map((t) => {
|
|
@@ -5601,11 +6473,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
5601
6473
|
function findBinding(start) {
|
|
5602
6474
|
let dir = start;
|
|
5603
6475
|
for (; ; ) {
|
|
5604
|
-
const path =
|
|
5605
|
-
if (
|
|
6476
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6477
|
+
if (existsSync16(path)) {
|
|
5606
6478
|
try {
|
|
5607
6479
|
const b = JSON.parse(
|
|
5608
|
-
|
|
6480
|
+
readFileSync12(path, "utf8")
|
|
5609
6481
|
);
|
|
5610
6482
|
if (b.decompositionId && b.taskId)
|
|
5611
6483
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5613,18 +6485,18 @@ function findBinding(start) {
|
|
|
5613
6485
|
}
|
|
5614
6486
|
return null;
|
|
5615
6487
|
}
|
|
5616
|
-
const parent =
|
|
6488
|
+
const parent = dirname11(dir);
|
|
5617
6489
|
if (parent === dir) return null;
|
|
5618
6490
|
dir = parent;
|
|
5619
6491
|
}
|
|
5620
6492
|
}
|
|
5621
6493
|
function parseTranscript(path) {
|
|
5622
|
-
if (!
|
|
6494
|
+
if (!existsSync16(path)) return null;
|
|
5623
6495
|
let tokensIn = 0;
|
|
5624
6496
|
let tokensOut = 0;
|
|
5625
6497
|
let contextUsed = 0;
|
|
5626
6498
|
let model = "";
|
|
5627
|
-
for (const line of
|
|
6499
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
5628
6500
|
if (!line.trim()) continue;
|
|
5629
6501
|
let obj;
|
|
5630
6502
|
try {
|
|
@@ -5641,11 +6513,61 @@ function parseTranscript(path) {
|
|
|
5641
6513
|
if (obj.message?.model) model = obj.message.model;
|
|
5642
6514
|
}
|
|
5643
6515
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5644
|
-
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
6516
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
|
|
5645
6517
|
}
|
|
5646
|
-
function windowFor(model) {
|
|
6518
|
+
function windowFor(model, contextUsed = 0) {
|
|
5647
6519
|
const m = model.toLowerCase();
|
|
5648
|
-
|
|
6520
|
+
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
6521
|
+
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
6522
|
+
}
|
|
6523
|
+
function buildHookEvents(input, usage, taskId) {
|
|
6524
|
+
const events = [];
|
|
6525
|
+
const base = (kind, over) => ({
|
|
6526
|
+
taskId,
|
|
6527
|
+
kind,
|
|
6528
|
+
tokensIn: null,
|
|
6529
|
+
tokensOut: null,
|
|
6530
|
+
contextUsed: null,
|
|
6531
|
+
contextWindow: null,
|
|
6532
|
+
text: null,
|
|
6533
|
+
approvalState: null,
|
|
6534
|
+
verdict: null,
|
|
6535
|
+
...over
|
|
6536
|
+
});
|
|
6537
|
+
if (usage) {
|
|
6538
|
+
events.push(
|
|
6539
|
+
base("Parsed", {
|
|
6540
|
+
tokensIn: usage.tokensIn,
|
|
6541
|
+
tokensOut: usage.tokensOut,
|
|
6542
|
+
contextUsed: usage.contextUsed,
|
|
6543
|
+
contextWindow: usage.contextWindow
|
|
6544
|
+
})
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
switch (input.hook_event_name) {
|
|
6548
|
+
case "PermissionDenied":
|
|
6549
|
+
events.push(
|
|
6550
|
+
base("Approval", {
|
|
6551
|
+
approvalState: "denied",
|
|
6552
|
+
text: input.tool_name ?? input.message ?? null
|
|
6553
|
+
})
|
|
6554
|
+
);
|
|
6555
|
+
break;
|
|
6556
|
+
case "Notification":
|
|
6557
|
+
if (isPermissionNotification(input))
|
|
6558
|
+
events.push(base("Approval", { text: input.message ?? null }));
|
|
6559
|
+
break;
|
|
6560
|
+
case "Stop":
|
|
6561
|
+
case "SubagentStop":
|
|
6562
|
+
events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
|
|
6563
|
+
break;
|
|
6564
|
+
}
|
|
6565
|
+
return events;
|
|
6566
|
+
}
|
|
6567
|
+
function isPermissionNotification(input) {
|
|
6568
|
+
const t = (input.notification_type ?? input.type ?? "").toLowerCase();
|
|
6569
|
+
if (t) return t.includes("permission");
|
|
6570
|
+
return (input.message ?? "").toLowerCase().includes("permission");
|
|
5649
6571
|
}
|
|
5650
6572
|
async function readStdin2() {
|
|
5651
6573
|
if (process.stdin.isTTY) return "";
|
|
@@ -5881,7 +6803,7 @@ Examples:
|
|
|
5881
6803
|
function resolveVersion() {
|
|
5882
6804
|
try {
|
|
5883
6805
|
const pkg = JSON.parse(
|
|
5884
|
-
|
|
6806
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
5885
6807
|
);
|
|
5886
6808
|
return pkg.version ?? "0.0.0";
|
|
5887
6809
|
} catch {
|
|
@@ -6039,6 +6961,9 @@ registerLookup(program);
|
|
|
6039
6961
|
registerRelationships(program);
|
|
6040
6962
|
registerWorkspace(program);
|
|
6041
6963
|
registerProject(program);
|
|
6964
|
+
registerDecomposition(program);
|
|
6965
|
+
registerExecutor(program);
|
|
6966
|
+
registerClose(program);
|
|
6042
6967
|
registerFiling(program);
|
|
6043
6968
|
registerContinuity(program);
|
|
6044
6969
|
registerCheckpoint(program);
|