@sechroom/cli 2026.8.8-rc.56311f4a3 → 2026.9.1
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 +917 -336
- package/package.json +1 -1
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 readFileSync20 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -642,6 +642,17 @@ function emitAction(summary, data, json) {
|
|
|
642
642
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
643
643
|
`);
|
|
644
644
|
}
|
|
645
|
+
var JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
646
|
+
function confidenceWireValue(value) {
|
|
647
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
648
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
649
|
+
const raw = String(value).trim();
|
|
650
|
+
if (JSON_NUMBER.test(raw)) {
|
|
651
|
+
const numeric = Number(raw);
|
|
652
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
653
|
+
}
|
|
654
|
+
return String(value);
|
|
655
|
+
}
|
|
645
656
|
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
646
657
|
function isGovernanceQueued(body) {
|
|
647
658
|
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
@@ -688,23 +699,57 @@ function formatFailureMessage(error) {
|
|
|
688
699
|
if (error.cause instanceof Error && error.cause.message) {
|
|
689
700
|
msg += `: ${error.cause.message}`;
|
|
690
701
|
}
|
|
691
|
-
} else if (
|
|
702
|
+
} else if (isRecord(error)) {
|
|
692
703
|
const problem = error;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
704
|
+
const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
|
|
705
|
+
const problemDetail2 = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
|
|
706
|
+
const fieldErrors = formatProblemErrors(problem.errors);
|
|
707
|
+
const structuredErrors = fieldErrors.length === 0 ? formatStructuredViolations(problem.violations) : [];
|
|
708
|
+
const parts = [
|
|
709
|
+
...title ? [title] : [],
|
|
710
|
+
...problemDetail2 ? [problemDetail2] : [],
|
|
711
|
+
...fieldErrors,
|
|
712
|
+
...structuredErrors
|
|
713
|
+
];
|
|
714
|
+
if (parts.length > 0) {
|
|
715
|
+
if (title && !problemDetail2 && fieldErrors.length === 0 && structuredErrors.length === 0) {
|
|
716
|
+
parts.push("No additional error detail was returned by the API.");
|
|
717
|
+
}
|
|
718
|
+
msg = parts.join("\n");
|
|
719
|
+
} else {
|
|
720
|
+
const serialized = JSON.stringify(error);
|
|
721
|
+
msg = serialized && serialized !== "{}" ? serialized : "The API returned an empty error response.";
|
|
700
722
|
}
|
|
701
|
-
} else if (typeof error === "object" && error !== null) {
|
|
702
|
-
msg = JSON.stringify(error);
|
|
703
723
|
} else {
|
|
704
724
|
msg = String(error);
|
|
705
725
|
}
|
|
706
726
|
return msg;
|
|
707
727
|
}
|
|
728
|
+
function isRecord(value) {
|
|
729
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
730
|
+
}
|
|
731
|
+
function formatProblemErrors(errors) {
|
|
732
|
+
if (!isRecord(errors)) return [];
|
|
733
|
+
return Object.entries(errors).flatMap(
|
|
734
|
+
([field, messages]) => (Array.isArray(messages) ? messages : [messages]).map(
|
|
735
|
+
(message) => ` ${field}: ${formatErrorValue(message)}`
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
function formatStructuredViolations(violations) {
|
|
740
|
+
if (!Array.isArray(violations)) return [];
|
|
741
|
+
return violations.flatMap((violation) => {
|
|
742
|
+
if (!isRecord(violation)) return [formatErrorValue(violation)];
|
|
743
|
+
const field = typeof violation.field === "string" ? violation.field : "validation";
|
|
744
|
+
const message = typeof violation.message === "string" ? violation.message : formatErrorValue(violation);
|
|
745
|
+
return [` ${field}: ${message}`];
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function formatErrorValue(value) {
|
|
749
|
+
if (typeof value === "string") return value;
|
|
750
|
+
const serialized = JSON.stringify(value);
|
|
751
|
+
return serialized ?? String(value);
|
|
752
|
+
}
|
|
708
753
|
function fail(error) {
|
|
709
754
|
const msg = formatFailureMessage(error);
|
|
710
755
|
process.stderr.write(`error: ${msg}
|
|
@@ -956,8 +1001,8 @@ function recordMaterialisedSkills(dir, slug2, skills, meta = {}) {
|
|
|
956
1001
|
}
|
|
957
1002
|
|
|
958
1003
|
// src/setup/materialise.ts
|
|
959
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as
|
|
960
|
-
import { join as
|
|
1004
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1005
|
+
import { join as join5 } from "path";
|
|
961
1006
|
|
|
962
1007
|
// src/setup/config-dirs.ts
|
|
963
1008
|
import { homedir as homedir2 } from "os";
|
|
@@ -1027,14 +1072,19 @@ async function fetchSetup(cfg, namespaceSlug) {
|
|
|
1027
1072
|
"/operator-surface/setup",
|
|
1028
1073
|
hasQuery ? { params: { query } } : {}
|
|
1029
1074
|
);
|
|
1030
|
-
if (error)
|
|
1075
|
+
if (error)
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`GET /operator-surface/setup failed: ${JSON.stringify(error)}`
|
|
1078
|
+
);
|
|
1031
1079
|
return data;
|
|
1032
1080
|
}
|
|
1033
1081
|
async function listNamespaces(cfg) {
|
|
1034
1082
|
const client = await makeClient(cfg);
|
|
1035
1083
|
const { data } = await client.GET("/mcp-aggregator/namespaces", {});
|
|
1036
1084
|
const rows = data ?? [];
|
|
1037
|
-
return rows.filter(
|
|
1085
|
+
return rows.filter(
|
|
1086
|
+
(r) => typeof r.slug === "string"
|
|
1087
|
+
).map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
|
|
1038
1088
|
}
|
|
1039
1089
|
function findSurface(setup, surfaceKey) {
|
|
1040
1090
|
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
@@ -1050,10 +1100,15 @@ function sectionSnippet(section) {
|
|
|
1050
1100
|
}
|
|
1051
1101
|
return null;
|
|
1052
1102
|
}
|
|
1053
|
-
function
|
|
1103
|
+
function parseTagArtifactCandidates(id) {
|
|
1054
1104
|
if (!id.startsWith("tag:")) return null;
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1105
|
+
const candidates = id.slice("tag:".length).split("|").map(
|
|
1106
|
+
(set) => set.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1107
|
+
).filter((set) => set.length > 0);
|
|
1108
|
+
return candidates.length > 0 ? candidates : null;
|
|
1109
|
+
}
|
|
1110
|
+
function parseTagArtifactId(id) {
|
|
1111
|
+
return parseTagArtifactCandidates(id)?.[0] ?? null;
|
|
1057
1112
|
}
|
|
1058
1113
|
async function getPersonalWorkspaceId(cfg) {
|
|
1059
1114
|
const client = await makeClient(cfg);
|
|
@@ -1062,40 +1117,102 @@ async function getPersonalWorkspaceId(cfg) {
|
|
|
1062
1117
|
}
|
|
1063
1118
|
async function fetchMemoryFields(cfg, id) {
|
|
1064
1119
|
const client = await makeClient(cfg);
|
|
1065
|
-
const { data } = await client.GET("/memories/{memoryId}", {
|
|
1120
|
+
const { data } = await client.GET("/memories/{memoryId}", {
|
|
1121
|
+
params: { path: { memoryId: id } }
|
|
1122
|
+
});
|
|
1066
1123
|
const env = data;
|
|
1067
1124
|
const m = env?.item ?? env;
|
|
1068
1125
|
if (!m) return null;
|
|
1069
1126
|
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1070
|
-
return {
|
|
1127
|
+
return {
|
|
1128
|
+
text: m.text,
|
|
1129
|
+
title: m.title,
|
|
1130
|
+
tags: m.tags,
|
|
1131
|
+
version: Number.isFinite(version) ? version : void 0
|
|
1132
|
+
};
|
|
1071
1133
|
}
|
|
1072
|
-
async function
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
const tags = parseTagArtifactId(artifact.id);
|
|
1076
|
-
if (!tags) continue;
|
|
1134
|
+
async function findTemplateByCandidates(client, candidates) {
|
|
1135
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1136
|
+
const tags = candidates[i];
|
|
1077
1137
|
const { data } = await client.POST("/memories/search", {
|
|
1078
|
-
body: {
|
|
1138
|
+
body: {
|
|
1139
|
+
query: null,
|
|
1140
|
+
textQuery: null,
|
|
1141
|
+
semanticQuery: null,
|
|
1142
|
+
hybrid: false,
|
|
1143
|
+
limit: 1,
|
|
1144
|
+
includeArchived: false,
|
|
1145
|
+
includeSystem: false,
|
|
1146
|
+
tags
|
|
1147
|
+
}
|
|
1079
1148
|
});
|
|
1080
1149
|
const hits = data?.items ?? [];
|
|
1081
1150
|
if (hits.length === 0) continue;
|
|
1082
|
-
|
|
1151
|
+
return { id: hits[0].id, tags, via: i === 0 ? "preferred" : "fallback" };
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function attemptedTagSets(section) {
|
|
1156
|
+
return section.artifacts.flatMap(
|
|
1157
|
+
(artifact) => parseTagArtifactCandidates(artifact.id) ?? []
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1161
|
+
const client = await makeClient(cfg);
|
|
1162
|
+
for (const artifact of section.artifacts) {
|
|
1163
|
+
const candidates = parseTagArtifactCandidates(artifact.id);
|
|
1164
|
+
if (!candidates) continue;
|
|
1165
|
+
const found = await findTemplateByCandidates(client, candidates);
|
|
1166
|
+
if (!found) continue;
|
|
1167
|
+
const templateId = found.id;
|
|
1083
1168
|
const template = await fetchMemoryFields(cfg, templateId);
|
|
1084
|
-
if (typeof template?.text !== "string" || template.text.length === 0)
|
|
1085
|
-
|
|
1169
|
+
if (typeof template?.text !== "string" || template.text.length === 0)
|
|
1170
|
+
continue;
|
|
1171
|
+
const templateTags = template.tags ?? found.tags;
|
|
1086
1172
|
if (personalWorkspaceId) {
|
|
1087
1173
|
const { data: ovr } = await client.POST("/memories/search", {
|
|
1088
|
-
body: {
|
|
1174
|
+
body: {
|
|
1175
|
+
query: null,
|
|
1176
|
+
textQuery: null,
|
|
1177
|
+
semanticQuery: null,
|
|
1178
|
+
hybrid: false,
|
|
1179
|
+
limit: 1,
|
|
1180
|
+
includeArchived: false,
|
|
1181
|
+
includeSystem: false,
|
|
1182
|
+
tags: [
|
|
1183
|
+
"sechroom:role:override",
|
|
1184
|
+
`sechroom:template-ref:${templateId}`
|
|
1185
|
+
],
|
|
1186
|
+
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1187
|
+
}
|
|
1089
1188
|
});
|
|
1090
1189
|
const ovrHits = ovr?.items ?? [];
|
|
1091
1190
|
if (ovrHits.length > 0) {
|
|
1092
1191
|
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1093
1192
|
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1094
|
-
return {
|
|
1193
|
+
return {
|
|
1194
|
+
title: override.title ?? template.title ?? artifact.title,
|
|
1195
|
+
body: override.text,
|
|
1196
|
+
source: "override",
|
|
1197
|
+
templateId,
|
|
1198
|
+
templateTags,
|
|
1199
|
+
resolvedVia: found.via,
|
|
1200
|
+
resolvedTags: found.tags,
|
|
1201
|
+
sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}`
|
|
1202
|
+
};
|
|
1095
1203
|
}
|
|
1096
1204
|
}
|
|
1097
1205
|
}
|
|
1098
|
-
return {
|
|
1206
|
+
return {
|
|
1207
|
+
title: template.title ?? artifact.title,
|
|
1208
|
+
body: template.text,
|
|
1209
|
+
source: "template",
|
|
1210
|
+
templateId,
|
|
1211
|
+
templateTags,
|
|
1212
|
+
resolvedVia: found.via,
|
|
1213
|
+
resolvedTags: found.tags,
|
|
1214
|
+
sourceRef: `${templateId}@v${template.version ?? 1}`
|
|
1215
|
+
};
|
|
1099
1216
|
}
|
|
1100
1217
|
return null;
|
|
1101
1218
|
}
|
|
@@ -1107,8 +1224,10 @@ async function resolveWorkspaceConventions(cfg, section) {
|
|
|
1107
1224
|
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1108
1225
|
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1109
1226
|
const ref = `${artifact.id}@v${mem.version ?? 1}`;
|
|
1110
|
-
parts.push(
|
|
1111
|
-
|
|
1227
|
+
parts.push(
|
|
1228
|
+
`<!-- @sechroom/cli:section source=${ref} -->
|
|
1229
|
+
${mem.text.trim()}`
|
|
1230
|
+
);
|
|
1112
1231
|
refs.push(ref);
|
|
1113
1232
|
}
|
|
1114
1233
|
}
|
|
@@ -1120,7 +1239,10 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1120
1239
|
const overrideTags = template.templateTags.filter(
|
|
1121
1240
|
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
1122
1241
|
);
|
|
1123
|
-
overrideTags.push(
|
|
1242
|
+
overrideTags.push(
|
|
1243
|
+
"sechroom:role:override",
|
|
1244
|
+
`sechroom:template-ref:${template.templateId}`
|
|
1245
|
+
);
|
|
1124
1246
|
const { error } = await client.POST("/memories", {
|
|
1125
1247
|
body: {
|
|
1126
1248
|
text: template.body,
|
|
@@ -1134,7 +1256,8 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1134
1256
|
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1135
1257
|
}
|
|
1136
1258
|
});
|
|
1137
|
-
if (error)
|
|
1259
|
+
if (error)
|
|
1260
|
+
throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1138
1261
|
}
|
|
1139
1262
|
|
|
1140
1263
|
// src/setup/skill-resolution.ts
|
|
@@ -1248,6 +1371,89 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1248
1371
|
return resolveReferences(rows.systemRows, rows.personalRows, surface);
|
|
1249
1372
|
}
|
|
1250
1373
|
|
|
1374
|
+
// src/setup/skill-orphans.ts
|
|
1375
|
+
import { readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, statSync } from "fs";
|
|
1376
|
+
import { join as join4 } from "path";
|
|
1377
|
+
var INSTALL_MARKER = "<!-- sechroom-install:";
|
|
1378
|
+
var MAX_MARKER_SCAN_BYTES = 2 * 1024 * 1024;
|
|
1379
|
+
function bundleFromBody(body) {
|
|
1380
|
+
const start = body.indexOf(INSTALL_MARKER);
|
|
1381
|
+
if (start < 0) return null;
|
|
1382
|
+
const from = start + INSTALL_MARKER.length;
|
|
1383
|
+
const end = body.indexOf("-->", from);
|
|
1384
|
+
if (end < 0) return null;
|
|
1385
|
+
try {
|
|
1386
|
+
const payload = JSON.parse(body.slice(from, end).replaceAll("\\", "").trim());
|
|
1387
|
+
return typeof payload.Bundle === "string" && payload.Bundle ? payload.Bundle : null;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function readBody(path) {
|
|
1393
|
+
try {
|
|
1394
|
+
if (statSync(path).size > MAX_MARKER_SCAN_BYTES) return null;
|
|
1395
|
+
return readFileSync3(path, "utf8");
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
var SKILL_LAYOUT = {
|
|
1401
|
+
scan: (dir) => readEntries(dir).filter((e) => e.isDirectory()).map((e) => ({ name: e.name, bodyPath: join4(dir, e.name, "SKILL.md") }))
|
|
1402
|
+
};
|
|
1403
|
+
var AGENT_LAYOUT = {
|
|
1404
|
+
scan: (dir) => readEntries(dir).filter((e) => e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".toml"))).map((e) => ({ name: e.name, bodyPath: join4(dir, e.name) }))
|
|
1405
|
+
};
|
|
1406
|
+
function readEntries(dir) {
|
|
1407
|
+
try {
|
|
1408
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
1409
|
+
} catch {
|
|
1410
|
+
return [];
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function scanMarked(dir, layout) {
|
|
1414
|
+
const marked = [];
|
|
1415
|
+
for (const item of layout.scan(dir)) {
|
|
1416
|
+
const body = readBody(item.bodyPath);
|
|
1417
|
+
if (body === null || !body.includes(INSTALL_MARKER)) continue;
|
|
1418
|
+
marked.push({ name: item.name, bundle: bundleFromBody(body) });
|
|
1419
|
+
}
|
|
1420
|
+
return marked;
|
|
1421
|
+
}
|
|
1422
|
+
function markedItems(dir, layout) {
|
|
1423
|
+
return scanMarked(dir, layout).map((item) => item.name);
|
|
1424
|
+
}
|
|
1425
|
+
function lockedNames(lock, exceptSlug) {
|
|
1426
|
+
const names = /* @__PURE__ */ new Set();
|
|
1427
|
+
for (const [slug2, entry] of Object.entries(lock)) {
|
|
1428
|
+
if (slug2 === exceptSlug) continue;
|
|
1429
|
+
for (const name of entry.skills ?? []) names.add(name);
|
|
1430
|
+
}
|
|
1431
|
+
return names;
|
|
1432
|
+
}
|
|
1433
|
+
function orphansAfterInstall(dir, layout, slug2, keep, ownBundles2 = /* @__PURE__ */ new Set()) {
|
|
1434
|
+
if (keep.length === 0) return [];
|
|
1435
|
+
const lock = readSkillsLock(dir);
|
|
1436
|
+
const protectedNames = /* @__PURE__ */ new Set([...keep, ...lockedNames(lock, slug2)]);
|
|
1437
|
+
const mine = scanMarked(dir, layout).filter((item) => ownBundles2.size === 0 || item.bundle === null || ownBundles2.has(item.bundle)).map((item) => item.name);
|
|
1438
|
+
const managed = /* @__PURE__ */ new Set([...lock[slug2]?.skills ?? [], ...mine]);
|
|
1439
|
+
return [...managed].filter((name) => !protectedNames.has(name)).sort();
|
|
1440
|
+
}
|
|
1441
|
+
function orphansAfterClean(dir, layout) {
|
|
1442
|
+
const protectedNames = lockedNames(readSkillsLock(dir));
|
|
1443
|
+
return markedItems(dir, layout).filter((name) => !protectedNames.has(name)).sort();
|
|
1444
|
+
}
|
|
1445
|
+
function pruneItems(dir, names) {
|
|
1446
|
+
const removed = [];
|
|
1447
|
+
for (const name of names) {
|
|
1448
|
+
try {
|
|
1449
|
+
rmSync2(join4(dir, name), { recursive: true, force: true });
|
|
1450
|
+
removed.push(name);
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return removed;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1251
1457
|
// src/setup/materialise.ts
|
|
1252
1458
|
var CLIENT_SURFACE = {
|
|
1253
1459
|
claude: "claude-code",
|
|
@@ -1256,8 +1462,8 @@ var CLIENT_SURFACE = {
|
|
|
1256
1462
|
function writeSkills(dir, skills, surface) {
|
|
1257
1463
|
const written = [];
|
|
1258
1464
|
for (const s of skills) {
|
|
1259
|
-
mkdirSync3(
|
|
1260
|
-
writeFileSync3(
|
|
1465
|
+
mkdirSync3(join5(dir, s.name), { recursive: true });
|
|
1466
|
+
writeFileSync3(join5(dir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
1261
1467
|
written.push(s.name);
|
|
1262
1468
|
}
|
|
1263
1469
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1307,14 +1513,17 @@ function codexAgentToml(agent) {
|
|
|
1307
1513
|
""
|
|
1308
1514
|
].join("\n");
|
|
1309
1515
|
}
|
|
1516
|
+
function agentFileName(name, surface) {
|
|
1517
|
+
return `${name}.${surface === CLIENT_SURFACE.codex ? "toml" : "md"}`;
|
|
1518
|
+
}
|
|
1310
1519
|
function writeAgents(dir, agents, surface) {
|
|
1311
1520
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1312
1521
|
const written = [];
|
|
1313
1522
|
for (const a of agents) {
|
|
1314
1523
|
const codex = surface === CLIENT_SURFACE.codex;
|
|
1315
|
-
const file =
|
|
1524
|
+
const file = agentFileName(a.name, surface);
|
|
1316
1525
|
const body = codex ? codexAgentToml(a) : a.body.endsWith("\n") ? a.body : a.body + "\n";
|
|
1317
|
-
writeFileSync3(
|
|
1526
|
+
writeFileSync3(join5(dir, file), body);
|
|
1318
1527
|
written.push(file);
|
|
1319
1528
|
}
|
|
1320
1529
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1323,10 +1532,10 @@ function writeAgents(dir, agents, surface) {
|
|
|
1323
1532
|
function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
1324
1533
|
if (!refs.length || !skills.length) return [];
|
|
1325
1534
|
for (const s of skills) {
|
|
1326
|
-
const refDir =
|
|
1535
|
+
const refDir = join5(dir, s.name, "references");
|
|
1327
1536
|
mkdirSync3(refDir, { recursive: true });
|
|
1328
1537
|
for (const r of refs) {
|
|
1329
|
-
writeFileSync3(
|
|
1538
|
+
writeFileSync3(join5(refDir, `${r.name}.md`), r.body.endsWith("\n") ? r.body : r.body + "\n");
|
|
1330
1539
|
}
|
|
1331
1540
|
}
|
|
1332
1541
|
return refs.map((r) => r.name);
|
|
@@ -1336,6 +1545,8 @@ var SKILL_SPEC = {
|
|
|
1336
1545
|
dir: skillsDir,
|
|
1337
1546
|
resolve: resolveSkillSet,
|
|
1338
1547
|
write: writeSkills,
|
|
1548
|
+
lockNames: (items) => items.map((i) => i.name),
|
|
1549
|
+
layout: SKILL_LAYOUT,
|
|
1339
1550
|
supportsCodex: true
|
|
1340
1551
|
};
|
|
1341
1552
|
var AGENT_SPEC = {
|
|
@@ -1343,8 +1554,18 @@ var AGENT_SPEC = {
|
|
|
1343
1554
|
dir: agentsDir,
|
|
1344
1555
|
resolve: resolveAgentSet,
|
|
1345
1556
|
write: writeAgents,
|
|
1557
|
+
lockNames: (items, surface) => items.map((i) => agentFileName(i.name, surface)),
|
|
1558
|
+
layout: AGENT_LAYOUT,
|
|
1346
1559
|
supportsCodex: true
|
|
1347
1560
|
};
|
|
1561
|
+
function ownBundles(items) {
|
|
1562
|
+
const bundles = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const item of items) {
|
|
1564
|
+
const bundle = bundleFromBody(item.body);
|
|
1565
|
+
if (bundle) bundles.add(bundle);
|
|
1566
|
+
}
|
|
1567
|
+
return bundles;
|
|
1568
|
+
}
|
|
1348
1569
|
function scopeOf(opts) {
|
|
1349
1570
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1350
1571
|
}
|
|
@@ -1424,8 +1645,16 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1424
1645
|
validateCodexWorkerDependencies(items, resolveAgentSet(rows, t.surface));
|
|
1425
1646
|
}
|
|
1426
1647
|
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1427
|
-
const
|
|
1648
|
+
const orphans = orphansAfterInstall(
|
|
1649
|
+
t.dir,
|
|
1650
|
+
spec.layout,
|
|
1651
|
+
DEFAULT_SKILLS_SLUG,
|
|
1652
|
+
spec.lockNames(items, t.surface),
|
|
1653
|
+
ownBundles(items)
|
|
1654
|
+
);
|
|
1655
|
+
const written = dryRun ? spec.lockNames(items, t.surface) : spec.write(t.dir, items, t.surface);
|
|
1428
1656
|
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1657
|
+
const pruned = dryRun ? orphans : pruneItems(t.dir, orphans);
|
|
1429
1658
|
return {
|
|
1430
1659
|
client: t.client,
|
|
1431
1660
|
surface: t.surface,
|
|
@@ -1436,7 +1665,8 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1436
1665
|
items: items.map(({ name, source }) => ({ name, source })),
|
|
1437
1666
|
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1438
1667
|
written,
|
|
1439
|
-
refsWritten
|
|
1668
|
+
refsWritten,
|
|
1669
|
+
pruned
|
|
1440
1670
|
};
|
|
1441
1671
|
});
|
|
1442
1672
|
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
@@ -1452,6 +1682,12 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1452
1682
|
console.log(
|
|
1453
1683
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1454
1684
|
);
|
|
1685
|
+
if (r.pruned.length) {
|
|
1686
|
+
console.log(
|
|
1687
|
+
style.yellow("! ") + `${dryRun ? "would prune" : "pruned"} ${r.pruned.length} superseded ${spec.kind}(s) no longer in the bundle ${style.dim("\u2192")} ${r.dir}`
|
|
1688
|
+
);
|
|
1689
|
+
for (const name of r.pruned) console.log(` ${style.yellow("-")} ${name}`);
|
|
1690
|
+
}
|
|
1455
1691
|
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1456
1692
|
}
|
|
1457
1693
|
}
|
|
@@ -1475,7 +1711,7 @@ function runList(spec, cmd, opts) {
|
|
|
1475
1711
|
const out = targets.map((t) => {
|
|
1476
1712
|
const lock = readSkillsLock(t.dir);
|
|
1477
1713
|
const entries = Object.entries(lock).flatMap(
|
|
1478
|
-
([slug2, e]) => (e.skills ?? []).map((name) => ({ slug: slug2, name, present: existsSync3(
|
|
1714
|
+
([slug2, e]) => (e.skills ?? []).map((name) => ({ slug: slug2, name, present: existsSync3(join5(t.dir, name)) }))
|
|
1479
1715
|
);
|
|
1480
1716
|
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1481
1717
|
});
|
|
@@ -1510,33 +1746,47 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1510
1746
|
} catch (err2) {
|
|
1511
1747
|
return fail(err2.message);
|
|
1512
1748
|
}
|
|
1749
|
+
const pruneOrphans = Boolean(opts.pruneOrphans);
|
|
1513
1750
|
const cleaned = [];
|
|
1514
1751
|
const missing = [];
|
|
1515
1752
|
for (const t of targets) {
|
|
1516
1753
|
const lock = readSkillsLock(t.dir);
|
|
1517
1754
|
const entry = lock[slug2];
|
|
1518
|
-
if (!entry) {
|
|
1519
|
-
missing.push(
|
|
1755
|
+
if (!entry && !pruneOrphans) {
|
|
1756
|
+
missing.push(join5(t.dir, SKILLS_LOCK));
|
|
1520
1757
|
continue;
|
|
1521
1758
|
}
|
|
1522
1759
|
const removed = [];
|
|
1523
|
-
for (const name of entry
|
|
1524
|
-
const p =
|
|
1760
|
+
for (const name of entry?.skills ?? []) {
|
|
1761
|
+
const p = join5(t.dir, name);
|
|
1525
1762
|
if (existsSync3(p)) {
|
|
1526
|
-
|
|
1763
|
+
rmSync3(p, { recursive: true, force: true });
|
|
1527
1764
|
removed.push(name);
|
|
1528
1765
|
}
|
|
1529
1766
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1767
|
+
if (entry) {
|
|
1768
|
+
delete lock[slug2];
|
|
1769
|
+
writeSkillsLock(t.dir, lock);
|
|
1770
|
+
}
|
|
1771
|
+
const pruned = pruneOrphans ? pruneItems(t.dir, orphansAfterClean(t.dir, spec.layout)) : [];
|
|
1772
|
+
if (!entry && pruned.length === 0) {
|
|
1773
|
+
missing.push(join5(t.dir, SKILLS_LOCK));
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed, pruned });
|
|
1533
1777
|
}
|
|
1534
1778
|
if (cleaned.length === 0) {
|
|
1535
1779
|
return fail(`No materialised ${spec.kind}s recorded for '${slug2}' in ${missing.join(", ")}.`);
|
|
1536
1780
|
}
|
|
1537
|
-
if (json) return emit({ kind: spec.kind, client: selection, slug: slug2, cleaned, missing }, true);
|
|
1781
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug: slug2, pruneOrphans, cleaned, missing }, true);
|
|
1538
1782
|
for (const c of cleaned) {
|
|
1539
1783
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug2} from ${c.dir}`));
|
|
1784
|
+
if (c.pruned.length) {
|
|
1785
|
+
console.log(
|
|
1786
|
+
style.yellow("! ") + `pruned ${c.pruned.length} orphaned ${spec.kind}(s) with no lock entry from ${c.dir}`
|
|
1787
|
+
);
|
|
1788
|
+
for (const name of c.pruned) console.log(` ${style.yellow("-")} ${name}`);
|
|
1789
|
+
}
|
|
1540
1790
|
}
|
|
1541
1791
|
}
|
|
1542
1792
|
|
|
@@ -1560,12 +1810,12 @@ target:gpt-codex-agent), the dispatchable workers your loop skills call
|
|
|
1560
1810
|
);
|
|
1561
1811
|
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));
|
|
1562
1812
|
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));
|
|
1563
|
-
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));
|
|
1813
|
+
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("--prune-orphans", "also remove sechroom-installed agent files that no lock entry claims (renamed/removed upstream)").option("--json", "machine output").option("--client <client>", "claude, codex, or all").action((slugArg, opts, cmd) => runClean(AGENT_SPEC, cmd, opts, slugArg));
|
|
1564
1814
|
}
|
|
1565
1815
|
|
|
1566
1816
|
// src/commands/channel.ts
|
|
1567
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as
|
|
1568
|
-
import { dirname as dirname9, join as
|
|
1817
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
1818
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
1569
1819
|
import {
|
|
1570
1820
|
HttpTransportType,
|
|
1571
1821
|
HubConnectionBuilder
|
|
@@ -1749,6 +1999,44 @@ async function runDriverLoop(ports, options) {
|
|
|
1749
1999
|
}
|
|
1750
2000
|
throw error;
|
|
1751
2001
|
}
|
|
2002
|
+
if (task.reviewExecution) {
|
|
2003
|
+
const binding = ports.verifyTaskBinding ? await ports.verifyTaskBinding(task) : {
|
|
2004
|
+
ok: false,
|
|
2005
|
+
reason: "review-execution task has no driver-side target verifier; refusing before turn"
|
|
2006
|
+
};
|
|
2007
|
+
if (!binding.ok) {
|
|
2008
|
+
const evidence = binding.evidence ? `
|
|
2009
|
+
|
|
2010
|
+
Observed binding:
|
|
2011
|
+
${binding.evidence}` : "";
|
|
2012
|
+
const text2 = `Review target preflight refused before turn. ${binding.reason ?? "repository/head mismatch"}${evidence}`;
|
|
2013
|
+
ports.log(
|
|
2014
|
+
`REVIEW TARGET REFUSED ${claim.memoryId}: ${binding.reason ?? "mismatch"}`
|
|
2015
|
+
);
|
|
2016
|
+
try {
|
|
2017
|
+
const done = await ports.completeLease(
|
|
2018
|
+
claim,
|
|
2019
|
+
"blocked",
|
|
2020
|
+
text2,
|
|
2021
|
+
`${task.title} \u2014 review target preflight refused`
|
|
2022
|
+
);
|
|
2023
|
+
summary.completed++;
|
|
2024
|
+
ports.log(
|
|
2025
|
+
`completed ${claim.memoryId} verdict:blocked \u2192 ${done.completionMemoryId ?? done.outcome}`
|
|
2026
|
+
);
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
if (e instanceof AuthExpiredError) throw e;
|
|
2029
|
+
summary.abandoned++;
|
|
2030
|
+
ports.log(
|
|
2031
|
+
`COMPLETE REJECTED for ${claim.memoryId} (${String(e)}) \u2014 refused review target will re-offer; investigate the lease gap.`
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
if (options.once) break;
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (binding.evidence)
|
|
2038
|
+
task = { ...task, reviewTargetEvidence: binding.evidence };
|
|
2039
|
+
}
|
|
1752
2040
|
const stopHeartbeat = ports.startLeaseHeartbeat(claim);
|
|
1753
2041
|
let result;
|
|
1754
2042
|
try {
|
|
@@ -1833,15 +2121,19 @@ Delivery: FAILED unexpectedly (${String(e)}) \u2014 changes remain in the execut
|
|
|
1833
2121
|
return summary;
|
|
1834
2122
|
}
|
|
1835
2123
|
function closeoutText(task, result) {
|
|
2124
|
+
const targetEvidence = task.reviewTargetEvidence ? `
|
|
2125
|
+
|
|
2126
|
+
Review target preflight evidence:
|
|
2127
|
+
${task.reviewTargetEvidence}` : "";
|
|
1836
2128
|
if (!result.packet)
|
|
1837
2129
|
return `Driven codex run ended without a sechroom_closeout packet (soft-fail). Last agent message:
|
|
1838
2130
|
|
|
1839
|
-
${result.lastAgentMessage || "(none)"}`;
|
|
2131
|
+
${result.lastAgentMessage || "(none)"}${targetEvidence}`;
|
|
1840
2132
|
const evidence = result.packet.evidence?.length ? `
|
|
1841
2133
|
|
|
1842
2134
|
Evidence:
|
|
1843
2135
|
${result.packet.evidence.map((e) => `- ${e}`).join("\n")}` : "";
|
|
1844
|
-
return `${result.packet.summary}${evidence}
|
|
2136
|
+
return `${result.packet.summary}${evidence}${targetEvidence}
|
|
1845
2137
|
|
|
1846
2138
|
(terminal_status: ${result.packet.terminal_status}; driven by sechroom executor run.)`;
|
|
1847
2139
|
}
|
|
@@ -1883,7 +2175,8 @@ async function materializeClaimedTask(request, memoryId) {
|
|
|
1883
2175
|
}
|
|
1884
2176
|
return {
|
|
1885
2177
|
title: card.title ?? memoryId,
|
|
1886
|
-
text: assemblePrompt(card, components)
|
|
2178
|
+
text: assemblePrompt(card, components),
|
|
2179
|
+
reviewExecution: card.reviewExecution ?? void 0
|
|
1887
2180
|
};
|
|
1888
2181
|
}
|
|
1889
2182
|
function validatePackage(value, expectedSlug, expectedVersion) {
|
|
@@ -1917,6 +2210,11 @@ ${card.task.boundaries}`,
|
|
|
1917
2210
|
`## Closeout
|
|
1918
2211
|
${card.task.closeout}`
|
|
1919
2212
|
];
|
|
2213
|
+
if (card.reviewExecution)
|
|
2214
|
+
sections.push(
|
|
2215
|
+
`## Review target
|
|
2216
|
+
${renderReviewExecution(card.reviewExecution)}`
|
|
2217
|
+
);
|
|
1920
2218
|
if (components.length > 0)
|
|
1921
2219
|
sections.push(
|
|
1922
2220
|
`## Task context
|
|
@@ -1924,6 +2222,17 @@ ${components.map(renderComponent).join("\n\n")}`
|
|
|
1924
2222
|
);
|
|
1925
2223
|
return sections.join("\n\n");
|
|
1926
2224
|
}
|
|
2225
|
+
function renderReviewExecution(target) {
|
|
2226
|
+
return [
|
|
2227
|
+
`kind: ${target.kind}`,
|
|
2228
|
+
`round: ${target.round}`,
|
|
2229
|
+
`repository: ${target.repository}`,
|
|
2230
|
+
`base commit: ${target.baseCommit}`,
|
|
2231
|
+
`head commit: ${target.headCommit}`,
|
|
2232
|
+
target.preferredInstanceKey ? `preferred instance: ${target.preferredInstanceKey}` : void 0,
|
|
2233
|
+
target.preferredLaneId ? `preferred lane: ${target.preferredLaneId}` : void 0
|
|
2234
|
+
].filter((line) => line !== void 0).join("\n");
|
|
2235
|
+
}
|
|
1927
2236
|
function renderComponent(component) {
|
|
1928
2237
|
return [
|
|
1929
2238
|
`<!-- sechroom-task-context component=${JSON.stringify(component.slug)} id=${JSON.stringify(component.sourceId)} sourceVersion=${component.sourceVersion} -->`,
|
|
@@ -1934,21 +2243,22 @@ function renderComponent(component) {
|
|
|
1934
2243
|
|
|
1935
2244
|
// src/commands/executor.ts
|
|
1936
2245
|
import { execFileSync } from "child_process";
|
|
1937
|
-
import {
|
|
1938
|
-
import {
|
|
2246
|
+
import { randomUUID } from "crypto";
|
|
2247
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
2248
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
1939
2249
|
|
|
1940
2250
|
// src/sem.ts
|
|
1941
|
-
import { dirname as dirname2, join as
|
|
1942
|
-
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as
|
|
1943
|
-
var SEM_FILE =
|
|
2251
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
2252
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2253
|
+
var SEM_FILE = join6(".sechroom", "lane.json");
|
|
1944
2254
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
1945
2255
|
function localSemPath(cwd = process.cwd()) {
|
|
1946
|
-
return
|
|
2256
|
+
return join6(cwd, SEM_FILE);
|
|
1947
2257
|
}
|
|
1948
2258
|
function resolveSemPathForRead(start = process.cwd()) {
|
|
1949
2259
|
let dir = start;
|
|
1950
2260
|
while (true) {
|
|
1951
|
-
const candidate =
|
|
2261
|
+
const candidate = join6(dir, SEM_FILE);
|
|
1952
2262
|
if (existsSync4(candidate)) return candidate;
|
|
1953
2263
|
const parent = dirname2(dir);
|
|
1954
2264
|
if (parent === dir) return void 0;
|
|
@@ -1960,7 +2270,7 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
1960
2270
|
let dir = start;
|
|
1961
2271
|
let gitPath;
|
|
1962
2272
|
for (; ; ) {
|
|
1963
|
-
const candidate =
|
|
2273
|
+
const candidate = join6(dir, ".git");
|
|
1964
2274
|
if (existsSync4(candidate)) {
|
|
1965
2275
|
gitPath = candidate;
|
|
1966
2276
|
break;
|
|
@@ -1969,14 +2279,14 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
1969
2279
|
if (parent === dir) break;
|
|
1970
2280
|
dir = parent;
|
|
1971
2281
|
}
|
|
1972
|
-
if (!gitPath ||
|
|
1973
|
-
const gitFile =
|
|
2282
|
+
if (!gitPath || statSync2(gitPath).isDirectory()) return lane;
|
|
2283
|
+
const gitFile = readFileSync4(gitPath, "utf8");
|
|
1974
2284
|
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1975
2285
|
if (!common) return lane;
|
|
1976
|
-
const worktreesDir =
|
|
1977
|
-
const siblings =
|
|
2286
|
+
const worktreesDir = join6(common[1], "worktrees");
|
|
2287
|
+
const siblings = readdirSync2(worktreesDir).filter((n) => {
|
|
1978
2288
|
try {
|
|
1979
|
-
return
|
|
2289
|
+
return statSync2(join6(worktreesDir, n)).isDirectory();
|
|
1980
2290
|
} catch {
|
|
1981
2291
|
return false;
|
|
1982
2292
|
}
|
|
@@ -1998,10 +2308,10 @@ function serializeSem(values) {
|
|
|
1998
2308
|
function readSem(path) {
|
|
1999
2309
|
const p = path ?? resolveSemPathForRead();
|
|
2000
2310
|
if (!p || !existsSync4(p)) return void 0;
|
|
2001
|
-
return { path: p, values: parseLaneJson(
|
|
2311
|
+
return { path: p, values: parseLaneJson(readFileSync4(p, "utf8")) };
|
|
2002
2312
|
}
|
|
2003
2313
|
function readLocalSemValues(cwd = process.cwd()) {
|
|
2004
|
-
const next =
|
|
2314
|
+
const next = join6(cwd, SEM_FILE);
|
|
2005
2315
|
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
2006
2316
|
return {};
|
|
2007
2317
|
}
|
|
@@ -2047,7 +2357,7 @@ var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
|
2047
2357
|
) + "\n";
|
|
2048
2358
|
function ensureContinuityScaffold(semPath) {
|
|
2049
2359
|
try {
|
|
2050
|
-
const target =
|
|
2360
|
+
const target = join6(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
2051
2361
|
if (existsSync4(target)) return;
|
|
2052
2362
|
writeFileSync4(target, CONTINUITY_SCAFFOLD);
|
|
2053
2363
|
} catch {
|
|
@@ -2062,7 +2372,7 @@ function ignoresSem(content) {
|
|
|
2062
2372
|
function inGitRepo(startDir) {
|
|
2063
2373
|
let dir = startDir;
|
|
2064
2374
|
for (; ; ) {
|
|
2065
|
-
if (existsSync4(
|
|
2375
|
+
if (existsSync4(join6(dir, ".git"))) return true;
|
|
2066
2376
|
const parent = dirname2(dir);
|
|
2067
2377
|
if (parent === dir) return false;
|
|
2068
2378
|
dir = parent;
|
|
@@ -2071,11 +2381,11 @@ function inGitRepo(startDir) {
|
|
|
2071
2381
|
function resolveGitignoreTarget(startDir) {
|
|
2072
2382
|
let dir = startDir;
|
|
2073
2383
|
for (; ; ) {
|
|
2074
|
-
const gi =
|
|
2384
|
+
const gi = join6(dir, ".gitignore");
|
|
2075
2385
|
if (existsSync4(gi)) return { path: gi, exists: true };
|
|
2076
2386
|
const parent = dirname2(dir);
|
|
2077
|
-
if (existsSync4(
|
|
2078
|
-
return { path:
|
|
2387
|
+
if (existsSync4(join6(dir, ".git")) || parent === dir) {
|
|
2388
|
+
return { path: join6(startDir, ".gitignore"), exists: false };
|
|
2079
2389
|
}
|
|
2080
2390
|
dir = parent;
|
|
2081
2391
|
}
|
|
@@ -2086,7 +2396,7 @@ function ensureSemIgnored(semPath) {
|
|
|
2086
2396
|
if (!inGitRepo(checkoutDir)) return;
|
|
2087
2397
|
const target = resolveGitignoreTarget(checkoutDir);
|
|
2088
2398
|
if (target.exists) {
|
|
2089
|
-
const content =
|
|
2399
|
+
const content = readFileSync4(target.path, "utf8");
|
|
2090
2400
|
if (ignoresSem(content)) return;
|
|
2091
2401
|
const sep2 = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2092
2402
|
appendFileSync(target.path, `${sep2}${STATE_DIR_IGNORE}
|
|
@@ -2100,7 +2410,7 @@ function ensureSemIgnored(semPath) {
|
|
|
2100
2410
|
}
|
|
2101
2411
|
|
|
2102
2412
|
// src/commands/executor-run.ts
|
|
2103
|
-
import { join as
|
|
2413
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
2104
2414
|
|
|
2105
2415
|
// src/executor-run/usage.ts
|
|
2106
2416
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
@@ -2719,7 +3029,9 @@ var CodexAppServer = class {
|
|
|
2719
3029
|
modelId,
|
|
2720
3030
|
executorInstanceId: this.options.executorInstanceId,
|
|
2721
3031
|
leaseId: telemetry?.leaseId ?? null,
|
|
2722
|
-
turnId: turnId || null
|
|
3032
|
+
turnId: turnId || null,
|
|
3033
|
+
originatingReviewId: telemetry?.originatingReviewId ?? null,
|
|
3034
|
+
reviewRound: telemetry?.reviewRound ?? null
|
|
2723
3035
|
});
|
|
2724
3036
|
this.onNotification = (msg) => {
|
|
2725
3037
|
if (msg.method === "thread/tokenUsage/updated") {
|
|
@@ -3243,8 +3555,8 @@ import {
|
|
|
3243
3555
|
closeSync,
|
|
3244
3556
|
mkdirSync as mkdirSync6,
|
|
3245
3557
|
openSync,
|
|
3246
|
-
readFileSync as
|
|
3247
|
-
rmSync as
|
|
3558
|
+
readFileSync as readFileSync5,
|
|
3559
|
+
rmSync as rmSync4,
|
|
3248
3560
|
writeFileSync as writeFileSync5
|
|
3249
3561
|
} from "fs";
|
|
3250
3562
|
import { readFile } from "fs/promises";
|
|
@@ -3253,7 +3565,7 @@ import {
|
|
|
3253
3565
|
createServer as createServer2
|
|
3254
3566
|
} from "net";
|
|
3255
3567
|
import { tmpdir } from "os";
|
|
3256
|
-
import { dirname as dirname4, join as
|
|
3568
|
+
import { dirname as dirname4, join as join7, resolve } from "path";
|
|
3257
3569
|
var DEFAULT_RESTART_POLICY = {
|
|
3258
3570
|
enabled: true,
|
|
3259
3571
|
maxRetries: 10,
|
|
@@ -4110,7 +4422,7 @@ function isProcessAlive(pid) {
|
|
|
4110
4422
|
function readPidFile(path) {
|
|
4111
4423
|
let raw;
|
|
4112
4424
|
try {
|
|
4113
|
-
raw =
|
|
4425
|
+
raw = readFileSync5(path, "utf8");
|
|
4114
4426
|
} catch (error) {
|
|
4115
4427
|
if (error.code === "ENOENT") return void 0;
|
|
4116
4428
|
throw error;
|
|
@@ -4124,7 +4436,7 @@ function claimPidFile(path, options = {}) {
|
|
|
4124
4436
|
});
|
|
4125
4437
|
let raw;
|
|
4126
4438
|
try {
|
|
4127
|
-
raw =
|
|
4439
|
+
raw = readFileSync5(path, "utf8");
|
|
4128
4440
|
} catch (error) {
|
|
4129
4441
|
if (error.code === "ENOENT") return;
|
|
4130
4442
|
throw error;
|
|
@@ -4138,7 +4450,7 @@ function claimPidFile(path, options = {}) {
|
|
|
4138
4450
|
log(
|
|
4139
4451
|
`stale pid-file ${path} (${valid ? `pid ${pid}` : "unparseable"} not running) \u2014 reclaiming`
|
|
4140
4452
|
);
|
|
4141
|
-
|
|
4453
|
+
rmSync4(path, { force: true });
|
|
4142
4454
|
}
|
|
4143
4455
|
function writePidFile(path, pid) {
|
|
4144
4456
|
mkdirSync6(dirname4(path), { recursive: true });
|
|
@@ -4148,7 +4460,7 @@ function writePidFile(path, pid) {
|
|
|
4148
4460
|
function removePidFileIfOwned(path, pid, log) {
|
|
4149
4461
|
if (readPidFile(path) !== pid) return;
|
|
4150
4462
|
try {
|
|
4151
|
-
|
|
4463
|
+
rmSync4(path, { force: true });
|
|
4152
4464
|
} catch (error) {
|
|
4153
4465
|
log?.(`pid-file ${path} cleanup failed: ${String(error)}`);
|
|
4154
4466
|
}
|
|
@@ -4198,12 +4510,12 @@ function launchDetachedSupervisor(options) {
|
|
|
4198
4510
|
}
|
|
4199
4511
|
function controlSocketPath(pidFile) {
|
|
4200
4512
|
const hash = createHash2("sha256").update(resolve(pidFile)).digest("hex").slice(0, 16);
|
|
4201
|
-
return
|
|
4513
|
+
return join7(tmpdir(), `sechroom-fleet-${hash}.sock`);
|
|
4202
4514
|
}
|
|
4203
4515
|
function startControlServer(socketPath, handlers, options = {}) {
|
|
4204
4516
|
const log = options.log ?? (() => {
|
|
4205
4517
|
});
|
|
4206
|
-
|
|
4518
|
+
rmSync4(socketPath, { force: true });
|
|
4207
4519
|
const dispatch = (req) => {
|
|
4208
4520
|
switch (req.command) {
|
|
4209
4521
|
case "status":
|
|
@@ -4265,7 +4577,7 @@ function startControlServer(socketPath, handlers, options = {}) {
|
|
|
4265
4577
|
socketPath,
|
|
4266
4578
|
close: () => new Promise((res) => {
|
|
4267
4579
|
server.close(() => {
|
|
4268
|
-
|
|
4580
|
+
rmSync4(socketPath, { force: true });
|
|
4269
4581
|
res();
|
|
4270
4582
|
});
|
|
4271
4583
|
})
|
|
@@ -4344,29 +4656,88 @@ function sendControlCommand(socketPath, request, options = {}) {
|
|
|
4344
4656
|
});
|
|
4345
4657
|
}
|
|
4346
4658
|
|
|
4659
|
+
// src/executor-run/review-target.ts
|
|
4660
|
+
async function verifyReviewTarget(git, task) {
|
|
4661
|
+
const target = task.reviewExecution;
|
|
4662
|
+
if (!target) return { ok: true };
|
|
4663
|
+
const [remote, head] = await Promise.all([
|
|
4664
|
+
git("git", ["remote", "get-url", "origin"]),
|
|
4665
|
+
git("git", ["rev-parse", "HEAD"])
|
|
4666
|
+
]);
|
|
4667
|
+
const observedRepository = remote.ok ? canonicalRepository(remote.stdout) : void 0;
|
|
4668
|
+
const observedHead = head.ok ? head.stdout.trim().toLowerCase() || void 0 : void 0;
|
|
4669
|
+
const failures = [];
|
|
4670
|
+
if (observedRepository !== target.repository)
|
|
4671
|
+
failures.push(
|
|
4672
|
+
`repository required ${target.repository}, observed ${observedRepository ?? "(unavailable)"}`
|
|
4673
|
+
);
|
|
4674
|
+
if (observedHead !== target.headCommit)
|
|
4675
|
+
failures.push(
|
|
4676
|
+
`head required ${target.headCommit}, observed ${observedHead ?? "(unavailable)"}`
|
|
4677
|
+
);
|
|
4678
|
+
const evidence = [
|
|
4679
|
+
`required repository: ${target.repository}`,
|
|
4680
|
+
`observed repository: ${observedRepository ?? "(unavailable)"}`,
|
|
4681
|
+
`required base: ${target.baseCommit}`,
|
|
4682
|
+
`required head: ${target.headCommit}`,
|
|
4683
|
+
`observed head: ${observedHead ?? "(unavailable)"}`
|
|
4684
|
+
].join("\n");
|
|
4685
|
+
return {
|
|
4686
|
+
ok: failures.length === 0,
|
|
4687
|
+
reason: failures.length > 0 ? `review target mismatch before turn: ${failures.join("; ")}` : void 0,
|
|
4688
|
+
evidence
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
function canonicalRepository(remote) {
|
|
4692
|
+
let value = remote.trim().split(/\r?\n/, 1)[0] ?? "";
|
|
4693
|
+
if (!value) return void 0;
|
|
4694
|
+
if (value.startsWith("git@")) {
|
|
4695
|
+
const separator = value.indexOf(":");
|
|
4696
|
+
if (separator < 0) return void 0;
|
|
4697
|
+
value = value.slice(separator + 1);
|
|
4698
|
+
} else {
|
|
4699
|
+
try {
|
|
4700
|
+
const parsed = new URL(value);
|
|
4701
|
+
if (parsed.protocol === "file:") return void 0;
|
|
4702
|
+
value = parsed.pathname;
|
|
4703
|
+
} catch {
|
|
4704
|
+
return void 0;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
value = value.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
4708
|
+
const parts = value.split("/").filter(Boolean);
|
|
4709
|
+
if (parts.length < 2) return void 0;
|
|
4710
|
+
const owner = parts.at(-2);
|
|
4711
|
+
const repository = parts.at(-1);
|
|
4712
|
+
if (!owner || !repository) return void 0;
|
|
4713
|
+
const part = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4714
|
+
if (!part.test(owner) || !part.test(repository)) return void 0;
|
|
4715
|
+
return `${owner}/${repository}`.toLowerCase();
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4347
4718
|
// src/commands/telemetry.ts
|
|
4348
4719
|
import {
|
|
4349
4720
|
existsSync as existsSync7,
|
|
4350
4721
|
mkdirSync as mkdirSync8,
|
|
4351
|
-
readFileSync as
|
|
4352
|
-
rmSync as
|
|
4722
|
+
readFileSync as readFileSync7,
|
|
4723
|
+
rmSync as rmSync5,
|
|
4353
4724
|
writeFileSync as writeFileSync7
|
|
4354
4725
|
} from "fs";
|
|
4355
4726
|
import { homedir as homedir4 } from "os";
|
|
4356
|
-
import { dirname as dirname7, join as
|
|
4727
|
+
import { dirname as dirname7, join as join10, parse, resolve as resolve2, sep } from "path";
|
|
4357
4728
|
|
|
4358
4729
|
// src/commands/hook-install.ts
|
|
4359
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as
|
|
4360
|
-
import { delimiter, dirname as dirname6, join as
|
|
4730
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4731
|
+
import { delimiter, dirname as dirname6, join as join9 } from "path";
|
|
4361
4732
|
|
|
4362
4733
|
// src/setup/clients.ts
|
|
4363
4734
|
import { existsSync as existsSync5 } from "fs";
|
|
4364
4735
|
import { homedir as homedir3 } from "os";
|
|
4365
|
-
import { dirname as dirname5, join as
|
|
4736
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
4366
4737
|
function claudeDesktopConfigPath(home) {
|
|
4367
4738
|
switch (process.platform) {
|
|
4368
4739
|
case "darwin":
|
|
4369
|
-
return
|
|
4740
|
+
return join8(
|
|
4370
4741
|
home,
|
|
4371
4742
|
"Library",
|
|
4372
4743
|
"Application Support",
|
|
@@ -4374,19 +4745,20 @@ function claudeDesktopConfigPath(home) {
|
|
|
4374
4745
|
"claude_desktop_config.json"
|
|
4375
4746
|
);
|
|
4376
4747
|
case "win32":
|
|
4377
|
-
return
|
|
4378
|
-
process.env.APPDATA ??
|
|
4748
|
+
return join8(
|
|
4749
|
+
process.env.APPDATA ?? join8(home, "AppData", "Roaming"),
|
|
4379
4750
|
"Claude",
|
|
4380
4751
|
"claude_desktop_config.json"
|
|
4381
4752
|
);
|
|
4382
4753
|
default:
|
|
4383
|
-
return
|
|
4754
|
+
return join8(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4384
4755
|
}
|
|
4385
4756
|
}
|
|
4386
4757
|
function clientTargets(cwd, opts = {}) {
|
|
4387
4758
|
const home = homedir3();
|
|
4388
|
-
const claudeDir = opts.claudeDir ??
|
|
4389
|
-
const codexHome = opts.codexHome === void 0 ?
|
|
4759
|
+
const claudeDir = opts.claudeDir ?? join8(home, ".claude");
|
|
4760
|
+
const codexHome = opts.codexHome === void 0 ? join8(home, ".codex") : opts.codexHome;
|
|
4761
|
+
const codexConfigPath = opts.codexScope === "project" ? join8(cwd, ".codex", "config.toml") : codexHome ? join8(codexHome, "config.toml") : null;
|
|
4390
4762
|
return {
|
|
4391
4763
|
"claude-code": {
|
|
4392
4764
|
key: "claude-code",
|
|
@@ -4394,10 +4766,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4394
4766
|
mcp: {
|
|
4395
4767
|
surfaceKey: "claude-code",
|
|
4396
4768
|
sectionType: SectionType.McpConfig,
|
|
4397
|
-
path:
|
|
4769
|
+
path: join8(cwd, ".mcp.json"),
|
|
4398
4770
|
format: "json"
|
|
4399
4771
|
},
|
|
4400
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
4772
|
+
instruction: { surfaceKey: "claude-code", path: join8(cwd, "CLAUDE.md") }
|
|
4401
4773
|
},
|
|
4402
4774
|
"claude-desktop": {
|
|
4403
4775
|
key: "claude-desktop",
|
|
@@ -4410,19 +4782,19 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4410
4782
|
},
|
|
4411
4783
|
instruction: {
|
|
4412
4784
|
surfaceKey: "claude-desktop",
|
|
4413
|
-
path:
|
|
4785
|
+
path: join8(claudeDir, "CLAUDE.md")
|
|
4414
4786
|
}
|
|
4415
4787
|
},
|
|
4416
4788
|
codex: {
|
|
4417
4789
|
key: "codex",
|
|
4418
4790
|
label: "Codex CLI",
|
|
4419
|
-
mcp:
|
|
4791
|
+
mcp: codexConfigPath ? {
|
|
4420
4792
|
surfaceKey: "chatgpt",
|
|
4421
4793
|
sectionType: SectionType.McpConfigToml,
|
|
4422
|
-
path:
|
|
4794
|
+
path: codexConfigPath,
|
|
4423
4795
|
format: "toml"
|
|
4424
4796
|
} : null,
|
|
4425
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
4797
|
+
instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
|
|
4426
4798
|
},
|
|
4427
4799
|
cursor: {
|
|
4428
4800
|
key: "cursor",
|
|
@@ -4430,10 +4802,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4430
4802
|
mcp: {
|
|
4431
4803
|
surfaceKey: "claude-code",
|
|
4432
4804
|
sectionType: SectionType.McpConfig,
|
|
4433
|
-
path:
|
|
4805
|
+
path: join8(cwd, ".cursor", "mcp.json"),
|
|
4434
4806
|
format: "json"
|
|
4435
4807
|
},
|
|
4436
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
4808
|
+
instruction: { surfaceKey: "chatgpt", path: join8(cwd, "AGENTS.md") }
|
|
4437
4809
|
},
|
|
4438
4810
|
antigravity: {
|
|
4439
4811
|
key: "antigravity",
|
|
@@ -4447,10 +4819,10 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4447
4819
|
mcp: {
|
|
4448
4820
|
surfaceKey: "antigravity",
|
|
4449
4821
|
sectionType: SectionType.McpConfig,
|
|
4450
|
-
path:
|
|
4822
|
+
path: join8(home, ".gemini", "config", "mcp_config.json"),
|
|
4451
4823
|
format: "json"
|
|
4452
4824
|
},
|
|
4453
|
-
instruction: { surfaceKey: "antigravity", path:
|
|
4825
|
+
instruction: { surfaceKey: "antigravity", path: join8(cwd, "AGENTS.md") }
|
|
4454
4826
|
}
|
|
4455
4827
|
};
|
|
4456
4828
|
}
|
|
@@ -4470,9 +4842,9 @@ function detectInstalledClients(cwd) {
|
|
|
4470
4842
|
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4471
4843
|
detected.push("claude-desktop");
|
|
4472
4844
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4473
|
-
if (existsSync5(
|
|
4845
|
+
if (existsSync5(join8(home, ".cursor")) || existsSync5(join8(cwd, ".cursor")))
|
|
4474
4846
|
detected.push("cursor");
|
|
4475
|
-
if (existsSync5(
|
|
4847
|
+
if (existsSync5(join8(home, ".gemini"))) detected.push("antigravity");
|
|
4476
4848
|
return detected;
|
|
4477
4849
|
}
|
|
4478
4850
|
|
|
@@ -4516,12 +4888,12 @@ function mergeHooks(config2, commands) {
|
|
|
4516
4888
|
}
|
|
4517
4889
|
function readJsonConfig2(path) {
|
|
4518
4890
|
if (!existsSync6(path)) return {};
|
|
4519
|
-
const raw =
|
|
4891
|
+
const raw = readFileSync6(path, "utf8");
|
|
4520
4892
|
if (!raw.trim()) return {};
|
|
4521
4893
|
return JSON.parse(raw);
|
|
4522
4894
|
}
|
|
4523
4895
|
function installHooksJson(path, commands, dryRun) {
|
|
4524
|
-
const existed = existsSync6(path) &&
|
|
4896
|
+
const existed = existsSync6(path) && readFileSync6(path, "utf8").trim().length > 0;
|
|
4525
4897
|
const config2 = readJsonConfig2(path);
|
|
4526
4898
|
const added = mergeHooks(config2, commands);
|
|
4527
4899
|
if (added === 0 && existed) return { path, status: "current" };
|
|
@@ -4532,12 +4904,12 @@ function installHooksJson(path, commands, dryRun) {
|
|
|
4532
4904
|
return { path, status: existed ? "merged" : "created" };
|
|
4533
4905
|
}
|
|
4534
4906
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
4535
|
-
return installHooksJson(
|
|
4907
|
+
return installHooksJson(join9(claudeDir, "settings.json"), commands, dryRun);
|
|
4536
4908
|
}
|
|
4537
4909
|
function installCodexCommands(codexHome, commands, dryRun) {
|
|
4538
4910
|
return [
|
|
4539
|
-
installHooksJson(
|
|
4540
|
-
installCodexFeatureFlag(
|
|
4911
|
+
installHooksJson(join9(codexHome, "hooks.json"), commands, dryRun),
|
|
4912
|
+
installCodexFeatureFlag(join9(codexHome, "config.toml"), dryRun)
|
|
4541
4913
|
];
|
|
4542
4914
|
}
|
|
4543
4915
|
function ensureCodexFeaturesHooks(content) {
|
|
@@ -4562,7 +4934,7 @@ function ensureCodexFeaturesHooks(content) {
|
|
|
4562
4934
|
}
|
|
4563
4935
|
function installCodexFeatureFlag(path, dryRun) {
|
|
4564
4936
|
const existed = existsSync6(path);
|
|
4565
|
-
const content = existed ?
|
|
4937
|
+
const content = existed ? readFileSync6(path, "utf8") : "";
|
|
4566
4938
|
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
4567
4939
|
if (!changed) return { path, status: "current" };
|
|
4568
4940
|
if (!dryRun) {
|
|
@@ -4592,11 +4964,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
4592
4964
|
const out = [];
|
|
4593
4965
|
for (const surface of surfaces) {
|
|
4594
4966
|
if (surface === "claude") {
|
|
4595
|
-
const path =
|
|
4967
|
+
const path = join9(opts.claudeDir, "settings.json");
|
|
4596
4968
|
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
4597
4969
|
} else {
|
|
4598
|
-
const hooksJson = installHooksJson(
|
|
4599
|
-
const featureFlag = installCodexFeatureFlag(
|
|
4970
|
+
const hooksJson = installHooksJson(join9(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
4971
|
+
const featureFlag = installCodexFeatureFlag(join9(opts.codexHome, "config.toml"), opts.dryRun);
|
|
4600
4972
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
4601
4973
|
}
|
|
4602
4974
|
}
|
|
@@ -4616,7 +4988,7 @@ function isSechroomOnPath() {
|
|
|
4616
4988
|
for (const dir of pathEnv.split(delimiter)) {
|
|
4617
4989
|
if (!dir) continue;
|
|
4618
4990
|
for (const name of names) {
|
|
4619
|
-
if (existsSync6(
|
|
4991
|
+
if (existsSync6(join9(dir, name))) return true;
|
|
4620
4992
|
}
|
|
4621
4993
|
}
|
|
4622
4994
|
return false;
|
|
@@ -4661,6 +5033,13 @@ function registerTelemetry(program2) {
|
|
|
4661
5033
|
).option("--text <s>", "Raw/parsed payload text").option("--approval <state>", "Approval gate state (approval events)").option(
|
|
4662
5034
|
"--verdict <v>",
|
|
4663
5035
|
"Typed verdict (terminal events): pass | soft-fail | plan-invalid | blocked"
|
|
5036
|
+
).option(
|
|
5037
|
+
"--originating-review-id <id>",
|
|
5038
|
+
"Existing review entity id that caused this rework; never creates a review"
|
|
5039
|
+
).option(
|
|
5040
|
+
"--review-round <n>",
|
|
5041
|
+
"Review round for the originating review",
|
|
5042
|
+
parseIntOpt
|
|
4664
5043
|
).action(async (opts, cmd) => {
|
|
4665
5044
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4666
5045
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -4673,7 +5052,9 @@ function registerTelemetry(program2) {
|
|
|
4673
5052
|
contextWindow: opts.contextWindow ?? null,
|
|
4674
5053
|
text: opts.text ?? null,
|
|
4675
5054
|
approvalState: opts.approval ?? null,
|
|
4676
|
-
verdict: opts.verdict ?? null
|
|
5055
|
+
verdict: opts.verdict ?? null,
|
|
5056
|
+
originatingReviewId: opts.originatingReviewId,
|
|
5057
|
+
reviewRound: opts.reviewRound
|
|
4677
5058
|
};
|
|
4678
5059
|
let body;
|
|
4679
5060
|
try {
|
|
@@ -4714,14 +5095,23 @@ function registerTelemetry(program2) {
|
|
|
4714
5095
|
).requiredOption(
|
|
4715
5096
|
"--decomposition <id>",
|
|
4716
5097
|
"Decomposition id this session executes"
|
|
4717
|
-
).requiredOption("--task <id>", "Task id this session executes").
|
|
5098
|
+
).requiredOption("--task <id>", "Task id this session executes").option(
|
|
5099
|
+
"--originating-review-id <id>",
|
|
5100
|
+
"Existing review entity id driving this rework; never creates a review"
|
|
5101
|
+
).option(
|
|
5102
|
+
"--review-round <n>",
|
|
5103
|
+
"Review round for the originating review",
|
|
5104
|
+
parseIntOpt
|
|
5105
|
+
).action((opts, cmd) => {
|
|
4718
5106
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4719
|
-
const dir =
|
|
5107
|
+
const dir = join10(process.cwd(), ".sechroom");
|
|
4720
5108
|
mkdirSync8(dir, { recursive: true });
|
|
4721
|
-
const path =
|
|
5109
|
+
const path = join10(dir, BINDING_FILE);
|
|
4722
5110
|
const binding = {
|
|
4723
5111
|
decompositionId: opts.decomposition,
|
|
4724
|
-
taskId: opts.task
|
|
5112
|
+
taskId: opts.task,
|
|
5113
|
+
originatingReviewId: opts.originatingReviewId,
|
|
5114
|
+
reviewRound: opts.reviewRound
|
|
4725
5115
|
};
|
|
4726
5116
|
writeFileSync7(path, JSON.stringify(binding, null, 2) + "\n");
|
|
4727
5117
|
ensureStateDirIgnored(process.cwd());
|
|
@@ -4738,9 +5128,9 @@ function registerTelemetry(program2) {
|
|
|
4738
5128
|
});
|
|
4739
5129
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
4740
5130
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
4741
|
-
const path =
|
|
5131
|
+
const path = join10(process.cwd(), ".sechroom", BINDING_FILE);
|
|
4742
5132
|
const existed = existsSync7(path);
|
|
4743
|
-
if (existed)
|
|
5133
|
+
if (existed) rmSync5(path);
|
|
4744
5134
|
if (json) emit({ unbound: existed, path }, true);
|
|
4745
5135
|
else
|
|
4746
5136
|
process.stdout.write(
|
|
@@ -4766,7 +5156,9 @@ function registerTelemetry(program2) {
|
|
|
4766
5156
|
input,
|
|
4767
5157
|
usage,
|
|
4768
5158
|
binding.taskId,
|
|
4769
|
-
configRoot
|
|
5159
|
+
configRoot,
|
|
5160
|
+
binding.originatingReviewId ?? null,
|
|
5161
|
+
binding.reviewRound ?? null
|
|
4770
5162
|
);
|
|
4771
5163
|
if (events.length === 0) return process.exit(0);
|
|
4772
5164
|
const taskId = await taskIdForHook(cfg, binding);
|
|
@@ -4860,11 +5252,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
4860
5252
|
function findBinding(start) {
|
|
4861
5253
|
let dir = start;
|
|
4862
5254
|
for (; ; ) {
|
|
4863
|
-
const path =
|
|
5255
|
+
const path = join10(dir, ".sechroom", BINDING_FILE);
|
|
4864
5256
|
if (existsSync7(path)) {
|
|
4865
5257
|
try {
|
|
4866
5258
|
const b = JSON.parse(
|
|
4867
|
-
|
|
5259
|
+
readFileSync7(path, "utf8")
|
|
4868
5260
|
);
|
|
4869
5261
|
if (b.decompositionId && b.taskId)
|
|
4870
5262
|
return {
|
|
@@ -4872,6 +5264,8 @@ function findBinding(start) {
|
|
|
4872
5264
|
taskId: b.taskId,
|
|
4873
5265
|
activeTaskCheckedAt: b.activeTaskCheckedAt,
|
|
4874
5266
|
lifecycleWarning: b.lifecycleWarning,
|
|
5267
|
+
originatingReviewId: b.originatingReviewId,
|
|
5268
|
+
reviewRound: b.reviewRound,
|
|
4875
5269
|
path
|
|
4876
5270
|
};
|
|
4877
5271
|
if (b.decompositionId && b.invalidatedTaskId && b.invalidatedReason === "terminal-task")
|
|
@@ -4879,6 +5273,8 @@ function findBinding(start) {
|
|
|
4879
5273
|
decompositionId: b.decompositionId,
|
|
4880
5274
|
invalidatedTaskId: b.invalidatedTaskId,
|
|
4881
5275
|
invalidatedReason: b.invalidatedReason,
|
|
5276
|
+
originatingReviewId: b.originatingReviewId,
|
|
5277
|
+
reviewRound: b.reviewRound,
|
|
4882
5278
|
path
|
|
4883
5279
|
};
|
|
4884
5280
|
} catch {
|
|
@@ -4941,7 +5337,7 @@ async function getTaskLifecycleVerdict(cfg, binding) {
|
|
|
4941
5337
|
function markLifecycleWarningIfCurrent(binding) {
|
|
4942
5338
|
try {
|
|
4943
5339
|
const current = JSON.parse(
|
|
4944
|
-
|
|
5340
|
+
readFileSync7(binding.path, "utf8")
|
|
4945
5341
|
);
|
|
4946
5342
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
4947
5343
|
return;
|
|
@@ -4949,7 +5345,9 @@ function markLifecycleWarningIfCurrent(binding) {
|
|
|
4949
5345
|
decompositionId: binding.decompositionId,
|
|
4950
5346
|
taskId: binding.taskId,
|
|
4951
5347
|
activeTaskCheckedAt: current.activeTaskCheckedAt,
|
|
4952
|
-
lifecycleWarning: "insufficient-permission"
|
|
5348
|
+
lifecycleWarning: "insufficient-permission",
|
|
5349
|
+
originatingReviewId: current.originatingReviewId,
|
|
5350
|
+
reviewRound: current.reviewRound
|
|
4953
5351
|
};
|
|
4954
5352
|
writeFileSync7(binding.path, JSON.stringify(warned, null, 2) + "\n");
|
|
4955
5353
|
} catch {
|
|
@@ -4958,14 +5356,16 @@ function markLifecycleWarningIfCurrent(binding) {
|
|
|
4958
5356
|
function cacheActiveBindingIfCurrent(binding) {
|
|
4959
5357
|
try {
|
|
4960
5358
|
const current = JSON.parse(
|
|
4961
|
-
|
|
5359
|
+
readFileSync7(binding.path, "utf8")
|
|
4962
5360
|
);
|
|
4963
5361
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
4964
5362
|
return;
|
|
4965
5363
|
const cached = {
|
|
4966
5364
|
decompositionId: binding.decompositionId,
|
|
4967
5365
|
taskId: binding.taskId,
|
|
4968
|
-
activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5366
|
+
activeTaskCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5367
|
+
originatingReviewId: current.originatingReviewId,
|
|
5368
|
+
reviewRound: current.reviewRound
|
|
4969
5369
|
};
|
|
4970
5370
|
writeFileSync7(binding.path, JSON.stringify(cached, null, 2) + "\n");
|
|
4971
5371
|
} catch {
|
|
@@ -4974,7 +5374,7 @@ function cacheActiveBindingIfCurrent(binding) {
|
|
|
4974
5374
|
function invalidateBindingIfCurrent(binding) {
|
|
4975
5375
|
try {
|
|
4976
5376
|
const current = JSON.parse(
|
|
4977
|
-
|
|
5377
|
+
readFileSync7(binding.path, "utf8")
|
|
4978
5378
|
);
|
|
4979
5379
|
if (current.decompositionId !== binding.decompositionId || current.taskId !== binding.taskId)
|
|
4980
5380
|
return;
|
|
@@ -4984,7 +5384,9 @@ function invalidateBindingIfCurrent(binding) {
|
|
|
4984
5384
|
{
|
|
4985
5385
|
decompositionId: binding.decompositionId,
|
|
4986
5386
|
invalidatedTaskId: binding.taskId,
|
|
4987
|
-
invalidatedReason: "terminal-task"
|
|
5387
|
+
invalidatedReason: "terminal-task",
|
|
5388
|
+
originatingReviewId: current.originatingReviewId,
|
|
5389
|
+
reviewRound: current.reviewRound
|
|
4988
5390
|
},
|
|
4989
5391
|
null,
|
|
4990
5392
|
2
|
|
@@ -4999,7 +5401,7 @@ function parseTranscript(path) {
|
|
|
4999
5401
|
let tokensOut = 0;
|
|
5000
5402
|
let contextUsed = 0;
|
|
5001
5403
|
let model = "";
|
|
5002
|
-
for (const line of
|
|
5404
|
+
for (const line of readFileSync7(path, "utf8").split("\n")) {
|
|
5003
5405
|
if (!line.trim()) continue;
|
|
5004
5406
|
let obj;
|
|
5005
5407
|
try {
|
|
@@ -5016,14 +5418,20 @@ function parseTranscript(path) {
|
|
|
5016
5418
|
if (obj.message?.model) model = obj.message.model;
|
|
5017
5419
|
}
|
|
5018
5420
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5019
|
-
return {
|
|
5421
|
+
return {
|
|
5422
|
+
tokensIn,
|
|
5423
|
+
tokensOut,
|
|
5424
|
+
contextUsed,
|
|
5425
|
+
contextWindow: windowFor(model, contextUsed),
|
|
5426
|
+
modelId: model || null
|
|
5427
|
+
};
|
|
5020
5428
|
}
|
|
5021
5429
|
function windowFor(model, contextUsed = 0) {
|
|
5022
5430
|
const m = model.toLowerCase();
|
|
5023
5431
|
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5024
5432
|
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5025
5433
|
}
|
|
5026
|
-
function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
5434
|
+
function buildHookEvents(input, usage, taskId, configRoot = null, originatingReviewId = null, reviewRound = null) {
|
|
5027
5435
|
const events = [];
|
|
5028
5436
|
const base = (kind, over) => ({
|
|
5029
5437
|
taskId,
|
|
@@ -5037,6 +5445,8 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
|
5037
5445
|
verdict: null,
|
|
5038
5446
|
modelId: null,
|
|
5039
5447
|
configRoot,
|
|
5448
|
+
originatingReviewId,
|
|
5449
|
+
reviewRound,
|
|
5040
5450
|
...over
|
|
5041
5451
|
});
|
|
5042
5452
|
if (usage) {
|
|
@@ -5065,7 +5475,9 @@ function buildHookEvents(input, usage, taskId, configRoot = null) {
|
|
|
5065
5475
|
break;
|
|
5066
5476
|
case "Stop":
|
|
5067
5477
|
case "SubagentStop":
|
|
5068
|
-
events.push(
|
|
5478
|
+
events.push(
|
|
5479
|
+
base("Terminal", { text: input.last_assistant_message ?? null })
|
|
5480
|
+
);
|
|
5069
5481
|
break;
|
|
5070
5482
|
}
|
|
5071
5483
|
return events;
|
|
@@ -5086,7 +5498,7 @@ function resolveClaudeConfigRoot(configuredRoot, transcriptPath) {
|
|
|
5086
5498
|
function normalizeClaudeConfigRoot(candidate) {
|
|
5087
5499
|
const trimmed = candidate?.trim();
|
|
5088
5500
|
if (!trimmed) return null;
|
|
5089
|
-
const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith(`~${sep}`) ?
|
|
5501
|
+
const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith(`~${sep}`) ? join10(homedir4(), trimmed.slice(2)) : trimmed;
|
|
5090
5502
|
let normalized = resolve2(expanded);
|
|
5091
5503
|
const rootLength = parse(normalized).root.length;
|
|
5092
5504
|
while (normalized.length > rootLength && normalized.endsWith(sep))
|
|
@@ -5160,10 +5572,10 @@ function registerExecutorRunCommand(executor) {
|
|
|
5160
5572
|
fail("--detach and --foreground are mutually exclusive");
|
|
5161
5573
|
const detach = !foreground;
|
|
5162
5574
|
const pidFile = resolve3(
|
|
5163
|
-
opts.pidFile ? String(opts.pidFile) :
|
|
5575
|
+
opts.pidFile ? String(opts.pidFile) : join11(process.cwd(), ".sechroom", "fleet.pid")
|
|
5164
5576
|
);
|
|
5165
5577
|
const logFile = resolve3(
|
|
5166
|
-
opts.logFile ? String(opts.logFile) :
|
|
5578
|
+
opts.logFile ? String(opts.logFile) : join11(process.cwd(), ".sechroom", "fleet.log")
|
|
5167
5579
|
);
|
|
5168
5580
|
if (detach && !isDetachedChild) {
|
|
5169
5581
|
await readFleetConfig(String(opts.config));
|
|
@@ -5449,7 +5861,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5449
5861
|
if (excludeTags.length)
|
|
5450
5862
|
log(`excluding offers tagged: ${excludeTags.join(", ")}`);
|
|
5451
5863
|
const rootDir = resolve3(String(opts.root));
|
|
5452
|
-
const usageLogPath =
|
|
5864
|
+
const usageLogPath = join11(
|
|
5453
5865
|
rootDir,
|
|
5454
5866
|
".sechroom",
|
|
5455
5867
|
`executor-usage-${located.state.instanceKey.replace(/[^\w.-]/g, "-")}.jsonl`
|
|
@@ -5579,6 +5991,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5579
5991
|
},
|
|
5580
5992
|
claimNext: async () => await fleetInbox.waitForClaim() ?? await claimNext(request, instance.id, log, excludeTags, skipLog),
|
|
5581
5993
|
loadTask: (memoryId) => materializeClaimedTask(request, memoryId),
|
|
5994
|
+
verifyTaskBinding: (task) => verifyReviewTarget(gitRunner, task),
|
|
5582
5995
|
startLeaseHeartbeat: (claim) => startLeaseHeartbeat(
|
|
5583
5996
|
() => request(
|
|
5584
5997
|
`/me/executor-task-leases/${encodeURIComponent(claim.leaseId)}/heartbeat`,
|
|
@@ -5707,7 +6120,7 @@ function registerExecutorRunCommand(executor) {
|
|
|
5707
6120
|
}
|
|
5708
6121
|
function resolveFleetPidFile(flag) {
|
|
5709
6122
|
return resolve3(
|
|
5710
|
-
flag ? String(flag) :
|
|
6123
|
+
flag ? String(flag) : join11(process.cwd(), ".sechroom", "fleet.pid")
|
|
5711
6124
|
);
|
|
5712
6125
|
}
|
|
5713
6126
|
function requireLiveSupervisor(pidFile) {
|
|
@@ -5843,6 +6256,13 @@ var CODEX_EXECUTOR_HOOKS = {
|
|
|
5843
6256
|
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
5844
6257
|
Stop: EXECUTOR_PULSE_COMMAND
|
|
5845
6258
|
};
|
|
6259
|
+
function claimNextHint(claim) {
|
|
6260
|
+
if (claim.outcome === "NoOffer")
|
|
6261
|
+
return "no offer is currently standing for this instance";
|
|
6262
|
+
if (claim.lease?.id && claim.claimToken)
|
|
6263
|
+
return `claimed lease ${claim.lease.id} \u2014 keep it alive with the claimToken above, then close it out with work_executor_complete`;
|
|
6264
|
+
return void 0;
|
|
6265
|
+
}
|
|
5846
6266
|
function registerExecutor(program2) {
|
|
5847
6267
|
const executor = program2.command("executor").description(
|
|
5848
6268
|
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
@@ -5955,8 +6375,8 @@ function registerExecutor(program2) {
|
|
|
5955
6375
|
fail("refresh-after must be shorter than the TTL");
|
|
5956
6376
|
const sem = readSem();
|
|
5957
6377
|
const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
|
|
5958
|
-
const statePath =
|
|
5959
|
-
const previous = existsSync8(statePath) ? JSON.parse(
|
|
6378
|
+
const statePath = join12(checkout, ".sechroom", EXECUTOR_STATE);
|
|
6379
|
+
const previous = existsSync8(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : void 0;
|
|
5960
6380
|
const canUpdateExisting = previous?.instanceId && previous.instanceKey === instanceKey && previous.laneId === laneId && previous.runtime === (runtime.toLowerCase() === "codex" ? "codex" : "claude-code") && previous.relayId === opts.relay && previous.connectorId === connector;
|
|
5961
6381
|
const state = {
|
|
5962
6382
|
schemaVersion: 1,
|
|
@@ -5985,8 +6405,8 @@ function registerExecutor(program2) {
|
|
|
5985
6405
|
}
|
|
5986
6406
|
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
|
|
5987
6407
|
(target) => target.dir
|
|
5988
|
-
) : [
|
|
5989
|
-
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [
|
|
6408
|
+
) : [join12(checkout, ".claude")];
|
|
6409
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join12(checkout, ".codex")];
|
|
5990
6410
|
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
5991
6411
|
for (const target of hookTargets) {
|
|
5992
6412
|
const results = surface === "claude" ? [
|
|
@@ -6201,6 +6621,29 @@ function registerExecutor(program2) {
|
|
|
6201
6621
|
);
|
|
6202
6622
|
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6203
6623
|
});
|
|
6624
|
+
executor.command("claim <id>").description(
|
|
6625
|
+
"Claim the next dispatch offer standing for this exact instance (one-shot attended self-claim)"
|
|
6626
|
+
).option(
|
|
6627
|
+
"--idempotency-key <key>",
|
|
6628
|
+
"Replay the same claim on retry instead of taking another offer (default: a fresh key per call)"
|
|
6629
|
+
).action(async (id, opts, cmd) => {
|
|
6630
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
6631
|
+
const data = await api(
|
|
6632
|
+
cfg,
|
|
6633
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers/claim-next`,
|
|
6634
|
+
{
|
|
6635
|
+
method: "POST",
|
|
6636
|
+
body: JSON.stringify({
|
|
6637
|
+
idempotencyKey: opts.idempotencyKey ?? `cli:${randomUUID().replace(/-/g, "")}`
|
|
6638
|
+
})
|
|
6639
|
+
}
|
|
6640
|
+
);
|
|
6641
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
6642
|
+
if (cmd.optsWithGlobals().json) return;
|
|
6643
|
+
const hint = claimNextHint(data);
|
|
6644
|
+
if (hint) process.stderr.write(style.dim(`${hint}
|
|
6645
|
+
`));
|
|
6646
|
+
});
|
|
6204
6647
|
executor.command("proxy-claim <generationId>").description(
|
|
6205
6648
|
"Node-held proxy claim: hold a task lease on a child's behalf so an attended/harness session is board-visible (node = holder-of-record, child = worker)"
|
|
6206
6649
|
).requiredOption(
|
|
@@ -6521,13 +6964,13 @@ function refreshRuntimeVersion(state, binaryOverride) {
|
|
|
6521
6964
|
function readExecutorState(start = process.cwd()) {
|
|
6522
6965
|
const semPath = resolveSemPathForRead(start);
|
|
6523
6966
|
const sem = semPath ? readSem(semPath) : void 0;
|
|
6524
|
-
const path =
|
|
6525
|
-
sem ? dirname8(sem.path) :
|
|
6967
|
+
const path = join12(
|
|
6968
|
+
sem ? dirname8(sem.path) : join12(start, ".sechroom"),
|
|
6526
6969
|
EXECUTOR_STATE
|
|
6527
6970
|
);
|
|
6528
6971
|
if (!existsSync8(path)) return void 0;
|
|
6529
6972
|
return {
|
|
6530
|
-
state: JSON.parse(
|
|
6973
|
+
state: JSON.parse(readFileSync8(path, "utf8")),
|
|
6531
6974
|
path
|
|
6532
6975
|
};
|
|
6533
6976
|
}
|
|
@@ -6697,7 +7140,7 @@ function registerChannel(program2) {
|
|
|
6697
7140
|
channel.command("install").description(
|
|
6698
7141
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
6699
7142
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
6700
|
-
const path =
|
|
7143
|
+
const path = join13(process.cwd(), ".mcp.json");
|
|
6701
7144
|
const dryRun = Boolean(opts.dryRun);
|
|
6702
7145
|
const name = "sechroom-channel";
|
|
6703
7146
|
const args = ["channel", "mcp"];
|
|
@@ -6794,36 +7237,69 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
6794
7237
|
}, intervalMilliseconds);
|
|
6795
7238
|
return () => cancel(timer);
|
|
6796
7239
|
}
|
|
7240
|
+
var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
|
|
7241
|
+
function classifyLeaseHeartbeatFailure(error) {
|
|
7242
|
+
if (!(error instanceof HttpError)) return "retry";
|
|
7243
|
+
if (error.status === 408 || error.status === 429) return "retry";
|
|
7244
|
+
if (error.status < 400 || error.status >= 500) return "retry";
|
|
7245
|
+
if (error.status === 409 && /\bLease is Released\b/.test(error.body))
|
|
7246
|
+
return "released";
|
|
7247
|
+
return "terminal";
|
|
7248
|
+
}
|
|
7249
|
+
function leaseHeartbeatStoppedLine(leaseId, error) {
|
|
7250
|
+
if (!(error instanceof HttpError))
|
|
7251
|
+
return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
|
|
7252
|
+
return `lease heartbeat stopped for ${leaseId} (${error.status}): ${problemDetail(error.body)}`;
|
|
7253
|
+
}
|
|
7254
|
+
function problemDetail(body) {
|
|
7255
|
+
try {
|
|
7256
|
+
const parsed = JSON.parse(body);
|
|
7257
|
+
const detail = parsed.detail ?? parsed.title;
|
|
7258
|
+
if (typeof detail === "string" && detail.length > 0) return detail;
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
return body;
|
|
7262
|
+
}
|
|
6797
7263
|
function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4, dependencies = {}) {
|
|
6798
7264
|
const leaseId = claim.lease?.id;
|
|
6799
7265
|
const claimToken = claim.claimToken;
|
|
6800
7266
|
if (!leaseId || !claimToken) return void 0;
|
|
6801
|
-
const request = dependencies.request ??
|
|
7267
|
+
const request = dependencies.request ?? leaseHeartbeatApi;
|
|
6802
7268
|
const onError = dependencies.onError ?? ((value) => process.stderr.write(
|
|
6803
7269
|
err(`channel lease heartbeat failed: ${String(value)}
|
|
6804
7270
|
`)
|
|
6805
7271
|
));
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
{
|
|
6821
|
-
|
|
6822
|
-
|
|
7272
|
+
let stop;
|
|
7273
|
+
const beat = async () => {
|
|
7274
|
+
try {
|
|
7275
|
+
return await request(
|
|
7276
|
+
cfg,
|
|
7277
|
+
`/me/executor-task-leases/${encodeURIComponent(leaseId)}/heartbeat`,
|
|
7278
|
+
{
|
|
7279
|
+
method: "POST",
|
|
7280
|
+
body: JSON.stringify({
|
|
7281
|
+
claimToken,
|
|
7282
|
+
tokenVersion: claim.tokenVersion ?? 1
|
|
7283
|
+
})
|
|
7284
|
+
}
|
|
7285
|
+
);
|
|
7286
|
+
} catch (error) {
|
|
7287
|
+
const disposition = classifyLeaseHeartbeatFailure(error);
|
|
7288
|
+
if (disposition === "retry") throw error;
|
|
7289
|
+
stop?.();
|
|
7290
|
+
dependencies.onLeaseTerminal?.(leaseId, error);
|
|
7291
|
+
if (disposition === "terminal")
|
|
7292
|
+
onError(leaseHeartbeatStoppedLine(leaseId, error));
|
|
7293
|
+
return void 0;
|
|
6823
7294
|
}
|
|
6824
|
-
|
|
7295
|
+
};
|
|
7296
|
+
stop = startLeaseHeartbeat(beat, onError, intervalMilliseconds, {
|
|
7297
|
+
setInterval: dependencies.setInterval,
|
|
7298
|
+
clearInterval: dependencies.clearInterval
|
|
7299
|
+
});
|
|
7300
|
+
return stop;
|
|
6825
7301
|
}
|
|
6826
|
-
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
7302
|
+
function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
|
|
6827
7303
|
const stops = /* @__PURE__ */ new Map();
|
|
6828
7304
|
const intervalMilliseconds = Math.max(
|
|
6829
7305
|
1e3,
|
|
@@ -6836,10 +7312,20 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds) {
|
|
|
6836
7312
|
const stop = startChannelTaskLeaseHeartbeat(
|
|
6837
7313
|
cfg,
|
|
6838
7314
|
claim,
|
|
6839
|
-
intervalMilliseconds
|
|
7315
|
+
intervalMilliseconds,
|
|
7316
|
+
{
|
|
7317
|
+
...dependencies,
|
|
7318
|
+
// The beat has already cancelled its own timer; drop the dead entry so a
|
|
7319
|
+
// later re-claim of the same lease id can start a fresh beat, and so the
|
|
7320
|
+
// map does not accumulate stopped leases for the life of the channel.
|
|
7321
|
+
onLeaseTerminal: (id) => {
|
|
7322
|
+
stops.delete(id);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
6840
7325
|
);
|
|
6841
7326
|
if (stop) stops.set(leaseId, stop);
|
|
6842
7327
|
},
|
|
7328
|
+
activeLeaseIds: () => [...stops.keys()],
|
|
6843
7329
|
stop: () => {
|
|
6844
7330
|
for (const stop of stops.values()) stop();
|
|
6845
7331
|
stops.clear();
|
|
@@ -6901,7 +7387,7 @@ async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {})
|
|
|
6901
7387
|
}
|
|
6902
7388
|
function readMcpConfig(path) {
|
|
6903
7389
|
if (!existsSync9(path)) return {};
|
|
6904
|
-
const raw =
|
|
7390
|
+
const raw = readFileSync9(path, "utf8");
|
|
6905
7391
|
if (!raw.trim()) return {};
|
|
6906
7392
|
try {
|
|
6907
7393
|
return JSON.parse(raw);
|
|
@@ -7075,12 +7561,12 @@ Examples:
|
|
|
7075
7561
|
|
|
7076
7562
|
// src/commands/checkpoint.ts
|
|
7077
7563
|
import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
|
|
7078
|
-
import { dirname as dirname14, join as
|
|
7564
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
7079
7565
|
|
|
7080
7566
|
// src/commands/hook.ts
|
|
7081
7567
|
import { createHash as createHash4 } from "crypto";
|
|
7082
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as
|
|
7083
|
-
import { dirname as dirname13, join as
|
|
7568
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
7569
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
7084
7570
|
|
|
7085
7571
|
// src/commands/lane-commit-hook.ts
|
|
7086
7572
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -7088,7 +7574,7 @@ import {
|
|
|
7088
7574
|
chmodSync,
|
|
7089
7575
|
existsSync as existsSync10,
|
|
7090
7576
|
mkdirSync as mkdirSync11,
|
|
7091
|
-
readFileSync as
|
|
7577
|
+
readFileSync as readFileSync10,
|
|
7092
7578
|
renameSync,
|
|
7093
7579
|
unlinkSync,
|
|
7094
7580
|
writeFileSync as writeFileSync10
|
|
@@ -7109,7 +7595,7 @@ function resolveCheckoutLane(start) {
|
|
|
7109
7595
|
return pin ? applyWorktreeLaneSuffix(pin, start) : void 0;
|
|
7110
7596
|
}
|
|
7111
7597
|
function appendLaneTrailer(messagePath, lane) {
|
|
7112
|
-
const original =
|
|
7598
|
+
const original = readFileSync10(messagePath, "utf8").replace(/\r\n/g, "\n");
|
|
7113
7599
|
const lines = original.split("\n");
|
|
7114
7600
|
const scissorsIndex = lines.findIndex(
|
|
7115
7601
|
(line) => /^#\s*-+\s*>8\s*-+\s*$/.test(line)
|
|
@@ -7181,7 +7667,7 @@ function resolveHookPath(root, hookName) {
|
|
|
7181
7667
|
function removeLegacyPrepareCommitMsgLeg(root) {
|
|
7182
7668
|
const path = resolveHookPath(root, LEGACY_HOOK_NAME);
|
|
7183
7669
|
if (!existsSync10(path)) return;
|
|
7184
|
-
const current =
|
|
7670
|
+
const current = readFileSync10(path, "utf8");
|
|
7185
7671
|
const pattern = managedBlockPattern();
|
|
7186
7672
|
if (!pattern.test(current)) return;
|
|
7187
7673
|
const next = current.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
@@ -7201,7 +7687,7 @@ function installLaneCommitHook(start) {
|
|
|
7201
7687
|
}).trim();
|
|
7202
7688
|
removeLegacyPrepareCommitMsgLeg(root);
|
|
7203
7689
|
const path = resolveHookPath(root, HOOK_NAME);
|
|
7204
|
-
const current = existsSync10(path) ?
|
|
7690
|
+
const current = existsSync10(path) ? readFileSync10(path, "utf8") : "";
|
|
7205
7691
|
let next;
|
|
7206
7692
|
if (current && !isShellHook(current)) {
|
|
7207
7693
|
const incumbentPath = nextIncumbentPath(path);
|
|
@@ -7224,25 +7710,25 @@ function installLaneCommitHook(start) {
|
|
|
7224
7710
|
}
|
|
7225
7711
|
|
|
7226
7712
|
// src/commands/session-context.ts
|
|
7227
|
-
import { randomUUID as
|
|
7228
|
-
import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as
|
|
7229
|
-
import { dirname as dirname12, join as
|
|
7713
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
7714
|
+
import { mkdirSync as mkdirSync13, renameSync as renameSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
7715
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
7230
7716
|
|
|
7231
7717
|
// src/setup/skill-composition-materialise.ts
|
|
7232
|
-
import { createHash as createHash3, randomUUID } from "crypto";
|
|
7718
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
7233
7719
|
import {
|
|
7234
7720
|
existsSync as existsSync11,
|
|
7235
7721
|
mkdirSync as mkdirSync12,
|
|
7236
|
-
readdirSync as
|
|
7237
|
-
readFileSync as
|
|
7722
|
+
readdirSync as readdirSync3,
|
|
7723
|
+
readFileSync as readFileSync11,
|
|
7238
7724
|
renameSync as renameSync2,
|
|
7239
7725
|
rmdirSync,
|
|
7240
|
-
rmSync as
|
|
7241
|
-
statSync as
|
|
7726
|
+
rmSync as rmSync6,
|
|
7727
|
+
statSync as statSync3,
|
|
7242
7728
|
writeFileSync as writeFileSync11
|
|
7243
7729
|
} from "fs";
|
|
7244
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, join as
|
|
7245
|
-
var COMPILED_SKILL_LOCK =
|
|
7730
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, join as join14, relative, resolve as resolve5 } from "path";
|
|
7731
|
+
var COMPILED_SKILL_LOCK = join14(
|
|
7246
7732
|
".sechroom",
|
|
7247
7733
|
"compiled-skill-materialisation.json"
|
|
7248
7734
|
);
|
|
@@ -7307,7 +7793,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7307
7793
|
desired.delete(key);
|
|
7308
7794
|
}
|
|
7309
7795
|
}
|
|
7310
|
-
const lockPath =
|
|
7796
|
+
const lockPath = join14(cwd, COMPILED_SKILL_LOCK);
|
|
7311
7797
|
const previous = readLock(lockPath, destinations);
|
|
7312
7798
|
const next = {
|
|
7313
7799
|
version: 2,
|
|
@@ -7318,7 +7804,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7318
7804
|
const path = skillPath(value.skillsRoot, value.skill.name);
|
|
7319
7805
|
const directory = dirname11(path);
|
|
7320
7806
|
const prior = previous.entries[key];
|
|
7321
|
-
const existing = existsSync11(path) ?
|
|
7807
|
+
const existing = existsSync11(path) ? readFileSync11(path, "utf8") : void 0;
|
|
7322
7808
|
const existingHash = existing === void 0 ? void 0 : sha256(existing);
|
|
7323
7809
|
const owned = prior !== void 0 && prior.skillsRoot === value.skillsRoot && existing !== void 0 && (existingHash === prior.contentHash || existingHash === prior.previousContentHash);
|
|
7324
7810
|
const recoverableMissing = prior !== void 0 && existing === void 0;
|
|
@@ -7366,7 +7852,7 @@ function materialiseCompiledSkillCompositions(cwd, payload, options = {}) {
|
|
|
7366
7852
|
};
|
|
7367
7853
|
writeLock(lockPath, next);
|
|
7368
7854
|
mkdirSync12(directory, { recursive: true });
|
|
7369
|
-
if (existingHash !== void 0 && (!existsSync11(path) || sha256(
|
|
7855
|
+
if (existingHash !== void 0 && (!existsSync11(path) || sha256(readFileSync11(path, "utf8")) !== existingHash)) {
|
|
7370
7856
|
delete next.entries[key];
|
|
7371
7857
|
writeLock(lockPath, next);
|
|
7372
7858
|
items.push({
|
|
@@ -7449,7 +7935,7 @@ function entryKey(target, name, skillsRoot) {
|
|
|
7449
7935
|
return `${target}:${sha256(skillsRoot)}:${name}`;
|
|
7450
7936
|
}
|
|
7451
7937
|
function skillPath(skillsRoot, name) {
|
|
7452
|
-
return
|
|
7938
|
+
return join14(skillsRoot, name, "SKILL.md");
|
|
7453
7939
|
}
|
|
7454
7940
|
function withFinalNewline(body) {
|
|
7455
7941
|
return body.endsWith("\n") ? body : body + "\n";
|
|
@@ -7464,7 +7950,7 @@ function readLock(path, destinations) {
|
|
|
7464
7950
|
)
|
|
7465
7951
|
);
|
|
7466
7952
|
try {
|
|
7467
|
-
const parsed = JSON.parse(
|
|
7953
|
+
const parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7468
7954
|
if (parsed.version === 2 && parsed.entries && typeof parsed.entries === "object") {
|
|
7469
7955
|
const entries = {};
|
|
7470
7956
|
for (const [key, candidate] of Object.entries(parsed.entries)) {
|
|
@@ -7495,22 +7981,22 @@ function writeAtomic(path, body) {
|
|
|
7495
7981
|
renameSync2(temporary, path);
|
|
7496
7982
|
}
|
|
7497
7983
|
function removeOwnedFile(path, acceptableHashes) {
|
|
7498
|
-
const quarantine = `${path}.sechroom-retire-${process.pid}-${
|
|
7984
|
+
const quarantine = `${path}.sechroom-retire-${process.pid}-${randomUUID2()}`;
|
|
7499
7985
|
try {
|
|
7500
7986
|
renameSync2(path, quarantine);
|
|
7501
7987
|
} catch (error) {
|
|
7502
7988
|
if (error.code === "ENOENT") return "missing";
|
|
7503
7989
|
throw error;
|
|
7504
7990
|
}
|
|
7505
|
-
const quarantinedHash = sha256(
|
|
7991
|
+
const quarantinedHash = sha256(readFileSync11(quarantine, "utf8"));
|
|
7506
7992
|
if (acceptableHashes.includes(quarantinedHash)) {
|
|
7507
|
-
|
|
7993
|
+
rmSync6(quarantine);
|
|
7508
7994
|
return "removed";
|
|
7509
7995
|
}
|
|
7510
7996
|
if (!existsSync11(path)) {
|
|
7511
7997
|
renameSync2(quarantine, path);
|
|
7512
7998
|
} else {
|
|
7513
|
-
renameSync2(quarantine, `${path}.sechroom-preserved-${
|
|
7999
|
+
renameSync2(quarantine, `${path}.sechroom-preserved-${randomUUID2()}`);
|
|
7514
8000
|
}
|
|
7515
8001
|
return "collision";
|
|
7516
8002
|
}
|
|
@@ -7519,7 +8005,7 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7519
8005
|
if (!path) return;
|
|
7520
8006
|
let current = "";
|
|
7521
8007
|
try {
|
|
7522
|
-
current =
|
|
8008
|
+
current = readFileSync11(path, "utf8");
|
|
7523
8009
|
} catch {
|
|
7524
8010
|
}
|
|
7525
8011
|
const withoutOwnedBlock = removeOwnedExcludeBlock(current);
|
|
@@ -7534,14 +8020,14 @@ function syncGitExcludes(cwd, lock) {
|
|
|
7534
8020
|
writeAtomic(path, updated);
|
|
7535
8021
|
}
|
|
7536
8022
|
function gitExcludePath(cwd) {
|
|
7537
|
-
const dotGit =
|
|
8023
|
+
const dotGit = join14(cwd, ".git");
|
|
7538
8024
|
try {
|
|
7539
|
-
if (
|
|
7540
|
-
const pointer =
|
|
8025
|
+
if (statSync3(dotGit).isDirectory()) return join14(dotGit, "info", "exclude");
|
|
8026
|
+
const pointer = readFileSync11(dotGit, "utf8").trim();
|
|
7541
8027
|
if (!pointer.startsWith("gitdir:")) return void 0;
|
|
7542
8028
|
const raw = pointer.slice("gitdir:".length).trim();
|
|
7543
8029
|
const gitDir = isAbsolute2(raw) ? raw : resolve5(cwd, raw);
|
|
7544
|
-
return
|
|
8030
|
+
return join14(gitDir, "info", "exclude");
|
|
7545
8031
|
} catch {
|
|
7546
8032
|
return void 0;
|
|
7547
8033
|
}
|
|
@@ -7558,13 +8044,13 @@ function removeOwnedExcludeBlock(body) {
|
|
|
7558
8044
|
}
|
|
7559
8045
|
function removeDirectoryIfEmpty(path) {
|
|
7560
8046
|
try {
|
|
7561
|
-
if (
|
|
8047
|
+
if (readdirSync3(path).length === 0) rmdirSync(path);
|
|
7562
8048
|
} catch {
|
|
7563
8049
|
}
|
|
7564
8050
|
}
|
|
7565
8051
|
|
|
7566
8052
|
// src/commands/session-context.ts
|
|
7567
|
-
var DYNAMIC_AGENT_CONTEXT_FILE =
|
|
8053
|
+
var DYNAMIC_AGENT_CONTEXT_FILE = join15(".sechroom", "CLAUDE.md");
|
|
7568
8054
|
function checkoutRoot(start) {
|
|
7569
8055
|
const semPath = resolveSemPathForRead(start);
|
|
7570
8056
|
return semPath ? dirname12(dirname12(semPath)) : start;
|
|
@@ -7601,7 +8087,7 @@ function renderSessionContext(result, lane) {
|
|
|
7601
8087
|
}
|
|
7602
8088
|
function writeSessionContext(start, lane, result, options = {}) {
|
|
7603
8089
|
const root = checkoutRoot(start);
|
|
7604
|
-
const path =
|
|
8090
|
+
const path = join15(root, DYNAMIC_AGENT_CONTEXT_FILE);
|
|
7605
8091
|
const skills = materialiseCompiledSkillCompositions(
|
|
7606
8092
|
root,
|
|
7607
8093
|
result.status === "hold" ? {
|
|
@@ -7617,13 +8103,13 @@ function writeSessionContext(start, lane, result, options = {}) {
|
|
|
7617
8103
|
);
|
|
7618
8104
|
const context = renderSessionContext(result, lane);
|
|
7619
8105
|
mkdirSync13(dirname12(path), { recursive: true });
|
|
7620
|
-
const temporaryPath = `${path}.${process.pid}.${
|
|
8106
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
7621
8107
|
try {
|
|
7622
8108
|
writeFileSync12(temporaryPath, context.endsWith("\n") ? context : `${context}
|
|
7623
8109
|
`, "utf8");
|
|
7624
8110
|
renameSync3(temporaryPath, path);
|
|
7625
8111
|
} catch (error) {
|
|
7626
|
-
|
|
8112
|
+
rmSync7(temporaryPath, { force: true });
|
|
7627
8113
|
throw error;
|
|
7628
8114
|
}
|
|
7629
8115
|
return { status: result.status, path, context, skills };
|
|
@@ -7674,11 +8160,12 @@ function resolveLane(flagLane, cwd) {
|
|
|
7674
8160
|
if (!base) return void 0;
|
|
7675
8161
|
return applyWorktreeLaneSuffix(base, start);
|
|
7676
8162
|
}
|
|
7677
|
-
var INTENT_FILE =
|
|
8163
|
+
var INTENT_FILE = join16(".sechroom", "continuity.json");
|
|
8164
|
+
var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
|
|
7678
8165
|
function resolveIntentPath(start) {
|
|
7679
8166
|
let dir = start;
|
|
7680
8167
|
for (; ; ) {
|
|
7681
|
-
const candidate =
|
|
8168
|
+
const candidate = join16(dir, INTENT_FILE);
|
|
7682
8169
|
if (existsSync12(candidate)) return candidate;
|
|
7683
8170
|
const parent = dirname13(dir);
|
|
7684
8171
|
if (parent === dir) return void 0;
|
|
@@ -7689,7 +8176,7 @@ function readIntent(start) {
|
|
|
7689
8176
|
const path = resolveIntentPath(start);
|
|
7690
8177
|
if (!path) return void 0;
|
|
7691
8178
|
try {
|
|
7692
|
-
return JSON.parse(
|
|
8179
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
7693
8180
|
} catch {
|
|
7694
8181
|
return void 0;
|
|
7695
8182
|
}
|
|
@@ -7699,6 +8186,16 @@ function hasRequiredIntent(i) {
|
|
|
7699
8186
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
7700
8187
|
);
|
|
7701
8188
|
}
|
|
8189
|
+
function localDryRunMissingFields(i) {
|
|
8190
|
+
const required = [
|
|
8191
|
+
["objective", "--objective"],
|
|
8192
|
+
["state", "--state"],
|
|
8193
|
+
["lastAction", "--last-action"],
|
|
8194
|
+
["nextAction", "--next-action"],
|
|
8195
|
+
["resumeInstruction", "--resume-instruction"]
|
|
8196
|
+
];
|
|
8197
|
+
return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
|
|
8198
|
+
}
|
|
7702
8199
|
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
7703
8200
|
const lane = resolveLane(laneFlag, cwd);
|
|
7704
8201
|
if (!lane) return false;
|
|
@@ -7720,7 +8217,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7720
8217
|
openQuestions: intent.questions ?? null,
|
|
7721
8218
|
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
7722
8219
|
relevantArtifactIds: intent.artifacts ?? null,
|
|
7723
|
-
|
|
8220
|
+
// Preserve invalid JSON-string confidence tokens for the server's semantic
|
|
8221
|
+
// validator; never coerce them through Number/NaN/null.
|
|
8222
|
+
confidence: confidenceWireValue(intent.confidence),
|
|
7724
8223
|
// Frequent triggers (compaction, session-end) land within the FR-051 4h
|
|
7725
8224
|
// window; Acknowledge lets the checkpoint persist on the lane.
|
|
7726
8225
|
concurrentSessionPolicy: "Acknowledge"
|
|
@@ -7731,14 +8230,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
7731
8230
|
}
|
|
7732
8231
|
function ledgerPath(start) {
|
|
7733
8232
|
const intent = resolveIntentPath(start);
|
|
7734
|
-
const dir = intent ? dirname13(intent) :
|
|
7735
|
-
return
|
|
8233
|
+
const dir = intent ? dirname13(intent) : join16(start, ".sechroom");
|
|
8234
|
+
return join16(dir, ".checkpoint-state.json");
|
|
7736
8235
|
}
|
|
7737
8236
|
function readLedger(start) {
|
|
7738
8237
|
try {
|
|
7739
8238
|
const p = ledgerPath(start);
|
|
7740
8239
|
if (!existsSync12(p)) return {};
|
|
7741
|
-
return JSON.parse(
|
|
8240
|
+
return JSON.parse(readFileSync12(p, "utf8"));
|
|
7742
8241
|
} catch {
|
|
7743
8242
|
return {};
|
|
7744
8243
|
}
|
|
@@ -7769,7 +8268,7 @@ function unchangedSinceLastPush(start, intent) {
|
|
|
7769
8268
|
const path = resolveIntentPath(start);
|
|
7770
8269
|
if (path && ledger.lastMtimeMs != null) {
|
|
7771
8270
|
try {
|
|
7772
|
-
if (
|
|
8271
|
+
if (statSync4(path).mtimeMs <= ledger.lastMtimeMs) return true;
|
|
7773
8272
|
} catch {
|
|
7774
8273
|
}
|
|
7775
8274
|
}
|
|
@@ -7781,7 +8280,7 @@ function recordPush(start, intent) {
|
|
|
7781
8280
|
const path = resolveIntentPath(start);
|
|
7782
8281
|
let mtimeMs;
|
|
7783
8282
|
try {
|
|
7784
|
-
if (path) mtimeMs =
|
|
8283
|
+
if (path) mtimeMs = statSync4(path).mtimeMs;
|
|
7785
8284
|
} catch {
|
|
7786
8285
|
mtimeMs = void 0;
|
|
7787
8286
|
}
|
|
@@ -8015,12 +8514,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
|
|
|
8015
8514
|
function registerCheckpoint(program2) {
|
|
8016
8515
|
program2.command("checkpoint").description(
|
|
8017
8516
|
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
8018
|
-
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
|
|
8517
|
+
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
|
|
8518
|
+
"--dry-run",
|
|
8519
|
+
"run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
|
|
8520
|
+
false
|
|
8521
|
+
).addHelpText(
|
|
8019
8522
|
"after",
|
|
8020
8523
|
`
|
|
8021
8524
|
File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
|
|
8022
8525
|
overrides that field. The snapshot is created FIRST (server-validated), then the local file is
|
|
8023
8526
|
written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
|
|
8527
|
+
--dry-run performs only the local required-field check and prints a payload. Its output is
|
|
8528
|
+
explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
|
|
8024
8529
|
|
|
8025
8530
|
Examples:
|
|
8026
8531
|
$ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
|
|
@@ -8043,7 +8548,9 @@ Examples:
|
|
|
8043
8548
|
questions: opts.question ?? base.questions,
|
|
8044
8549
|
surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
|
|
8045
8550
|
artifacts: opts.artifact ?? base.artifacts,
|
|
8046
|
-
|
|
8551
|
+
// Keep the raw token until the server's shared validator sees it. In particular,
|
|
8552
|
+
// Number("high") -> NaN -> JSON null would silently discard the operator's input.
|
|
8553
|
+
confidence: opts.confidence != null ? opts.confidence : base.confidence
|
|
8047
8554
|
};
|
|
8048
8555
|
const lane = resolveLane(opts.lane, cwd);
|
|
8049
8556
|
if (!lane) {
|
|
@@ -8051,19 +8558,6 @@ Examples:
|
|
|
8051
8558
|
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
|
|
8052
8559
|
);
|
|
8053
8560
|
}
|
|
8054
|
-
const required = [
|
|
8055
|
-
["objective", "--objective"],
|
|
8056
|
-
["state", "--state"],
|
|
8057
|
-
["lastAction", "--last-action"],
|
|
8058
|
-
["nextAction", "--next-action"],
|
|
8059
|
-
["resumeInstruction", "--resume-instruction"]
|
|
8060
|
-
];
|
|
8061
|
-
const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
|
|
8062
|
-
if (missing.length > 0) {
|
|
8063
|
-
fail(
|
|
8064
|
-
`missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
8065
|
-
);
|
|
8066
|
-
}
|
|
8067
8561
|
const scope = merged.scope ?? "session";
|
|
8068
8562
|
const body = {
|
|
8069
8563
|
laneId: lane,
|
|
@@ -8077,20 +8571,40 @@ Examples:
|
|
|
8077
8571
|
openQuestions: merged.questions ?? null,
|
|
8078
8572
|
surfaceMarkers: merged.surfaceMarkers ?? null,
|
|
8079
8573
|
relevantArtifactIds: merged.artifacts ?? null,
|
|
8080
|
-
confidence: merged.confidence
|
|
8574
|
+
confidence: confidenceWireValue(merged.confidence),
|
|
8081
8575
|
// Explicit checkpoints are often within the FR-051 4h window; Acknowledge
|
|
8082
8576
|
// lets one land on the lane (matches `hook pre-compact`).
|
|
8083
8577
|
concurrentSessionPolicy: "Acknowledge"
|
|
8084
8578
|
};
|
|
8085
8579
|
if (opts.dryRun) {
|
|
8086
|
-
|
|
8580
|
+
const missing = localDryRunMissingFields(merged);
|
|
8581
|
+
if (missing.length > 0) {
|
|
8582
|
+
fail(
|
|
8583
|
+
`LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
8584
|
+
);
|
|
8585
|
+
}
|
|
8586
|
+
emit(
|
|
8587
|
+
{
|
|
8588
|
+
dryRun: true,
|
|
8589
|
+
validation: {
|
|
8590
|
+
mode: "local-only",
|
|
8591
|
+
serverValidated: false,
|
|
8592
|
+
checks: ["required-fields"],
|
|
8593
|
+
warning: LOCAL_DRY_RUN_VALIDATION_WARNING
|
|
8594
|
+
},
|
|
8595
|
+
lane,
|
|
8596
|
+
scope,
|
|
8597
|
+
wouldCreate: body
|
|
8598
|
+
},
|
|
8599
|
+
json
|
|
8600
|
+
);
|
|
8087
8601
|
return;
|
|
8088
8602
|
}
|
|
8089
8603
|
const data = await runApi("Creating snapshot", async () => {
|
|
8090
8604
|
const client = await makeClient(cfg);
|
|
8091
8605
|
return client.POST("/continuity/snapshots", { body });
|
|
8092
8606
|
});
|
|
8093
|
-
const path = resolveIntentPath(cwd) ??
|
|
8607
|
+
const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
|
|
8094
8608
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
8095
8609
|
mkdirSync15(dirname14(path), { recursive: true });
|
|
8096
8610
|
writeFileSync14(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
@@ -8107,7 +8621,7 @@ Examples:
|
|
|
8107
8621
|
}
|
|
8108
8622
|
|
|
8109
8623
|
// src/commands/close.ts
|
|
8110
|
-
import { readFileSync as
|
|
8624
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
8111
8625
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
8112
8626
|
function registerClose(program2) {
|
|
8113
8627
|
program2.command("close").description(
|
|
@@ -8148,7 +8662,7 @@ Examples:
|
|
|
8148
8662
|
);
|
|
8149
8663
|
let bodyText;
|
|
8150
8664
|
try {
|
|
8151
|
-
bodyText = opts.file ?
|
|
8665
|
+
bodyText = opts.file ? readFileSync13(opts.file, "utf8") : readFileSync13(0, "utf8");
|
|
8152
8666
|
} catch {
|
|
8153
8667
|
fail(
|
|
8154
8668
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -8320,7 +8834,7 @@ Displaced snapshot recovery surfaces:
|
|
|
8320
8834
|
openQuestions: opts.question ?? null,
|
|
8321
8835
|
surfaceMarkers: opts.surfaceMarker ?? null,
|
|
8322
8836
|
relevantArtifactIds: opts.artifact ?? null,
|
|
8323
|
-
confidence:
|
|
8837
|
+
confidence: confidenceWireValue(opts.confidence)
|
|
8324
8838
|
}
|
|
8325
8839
|
});
|
|
8326
8840
|
});
|
|
@@ -8619,7 +9133,7 @@ Examples:
|
|
|
8619
9133
|
);
|
|
8620
9134
|
});
|
|
8621
9135
|
workPlan.command("append <decompositionId>").description(
|
|
8622
|
-
"Append hand-authored task(s) to a running work plan; they land proposed until ratified (POST /decompositions/{id}/append-tasks)"
|
|
9136
|
+
"Append hand-authored task(s) to a running work plan; they land proposed until ratified. Verification tasks may carry reviewExecution { kind: local-code-review, round, repository, baseCommit, headCommit, preferredInstanceKey?, preferredLaneId? } (POST /decompositions/{id}/append-tasks)"
|
|
8623
9137
|
).requiredOption(
|
|
8624
9138
|
"--file <path>",
|
|
8625
9139
|
"JSON file containing { tasks, gates? } (the AppendTasksInput shape); use - for stdin"
|
|
@@ -9018,14 +9532,34 @@ Examples:
|
|
|
9018
9532
|
});
|
|
9019
9533
|
}
|
|
9020
9534
|
|
|
9535
|
+
// src/commands/github.ts
|
|
9536
|
+
function registerGitHub(program2) {
|
|
9537
|
+
const installation = program2.command("github").description("Manage the tenant's GitHub App installation").command("installation").description("Inspect or administer the tenant's GitHub App installation");
|
|
9538
|
+
installation.command("revoke <installationId>").description("Revoke a GitHub App installation (POST /github/app/installations/{installationId}/revoke)").option("--reason <reason>", "Optional reason recorded in the installation audit trail").action(async (installationId, opts, cmd) => {
|
|
9539
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
9540
|
+
const data = await runApi("Revoking GitHub App installation", async () => {
|
|
9541
|
+
const client = await makeClient(cfg);
|
|
9542
|
+
return client.POST("/github/app/installations/{installationId}/revoke", {
|
|
9543
|
+
params: { path: { installationId } },
|
|
9544
|
+
body: { reason: opts.reason ?? null }
|
|
9545
|
+
});
|
|
9546
|
+
});
|
|
9547
|
+
emitAction(
|
|
9548
|
+
`revoked GitHub App installation ${style.bold(installationId)}`,
|
|
9549
|
+
data,
|
|
9550
|
+
cmd.optsWithGlobals().json
|
|
9551
|
+
);
|
|
9552
|
+
});
|
|
9553
|
+
}
|
|
9554
|
+
|
|
9021
9555
|
// src/commands/herdr.ts
|
|
9022
|
-
import { readFileSync as
|
|
9556
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
9023
9557
|
import { basename as basename3 } from "path";
|
|
9024
9558
|
|
|
9025
9559
|
// src/herdr/client.ts
|
|
9026
9560
|
import { createConnection as createConnection2 } from "net";
|
|
9027
9561
|
import { homedir as homedir5 } from "os";
|
|
9028
|
-
import { join as
|
|
9562
|
+
import { join as join18 } from "path";
|
|
9029
9563
|
var DEFAULT_HERDR_SOCKET_RELATIVE = ".config/herdr/herdr.sock";
|
|
9030
9564
|
var HerdrUnreachableError = class extends Error {
|
|
9031
9565
|
constructor(socketPath, reason) {
|
|
@@ -9050,7 +9584,7 @@ function resolveSocketPath(flag, env = process.env, home = homedir5()) {
|
|
|
9050
9584
|
if (fromFlag) return fromFlag;
|
|
9051
9585
|
const fromEnv = env.HERDR_SOCKET?.trim();
|
|
9052
9586
|
if (fromEnv) return fromEnv;
|
|
9053
|
-
return
|
|
9587
|
+
return join18(home, DEFAULT_HERDR_SOCKET_RELATIVE);
|
|
9054
9588
|
}
|
|
9055
9589
|
function expandTarget(target) {
|
|
9056
9590
|
const trimmed = target.trim();
|
|
@@ -9390,7 +9924,7 @@ function parseSource(value, fallback = DEFAULT_READ_SOURCE) {
|
|
|
9390
9924
|
}
|
|
9391
9925
|
return match;
|
|
9392
9926
|
}
|
|
9393
|
-
function resolveSendText(textArgs, useStdin, readStdin5 = () =>
|
|
9927
|
+
function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync14(0, "utf8")) {
|
|
9394
9928
|
if (useStdin) {
|
|
9395
9929
|
if (textArgs.length > 0) {
|
|
9396
9930
|
throw new Error(
|
|
@@ -10130,11 +10664,11 @@ Examples:
|
|
|
10130
10664
|
}
|
|
10131
10665
|
|
|
10132
10666
|
// src/commands/memory.ts
|
|
10133
|
-
import { readFileSync as
|
|
10667
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
10134
10668
|
|
|
10135
10669
|
// src/commands/memory-import.ts
|
|
10136
|
-
import { readdirSync as
|
|
10137
|
-
import { basename as basename4, join as
|
|
10670
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync, statSync as statSync5 } from "fs";
|
|
10671
|
+
import { basename as basename4, join as join19, resolve as resolve6 } from "path";
|
|
10138
10672
|
var MARKDOWN_RE = /\.(md|markdown)$/i;
|
|
10139
10673
|
function isMarkdownPath(path) {
|
|
10140
10674
|
return MARKDOWN_RE.test(path);
|
|
@@ -10161,18 +10695,18 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10161
10695
|
files.push(path);
|
|
10162
10696
|
};
|
|
10163
10697
|
const walk = (dir) => {
|
|
10164
|
-
const entries =
|
|
10698
|
+
const entries = readdirSync4(dir, { withFileTypes: true }).sort(
|
|
10165
10699
|
(a, b) => a.name.localeCompare(b.name)
|
|
10166
10700
|
);
|
|
10167
10701
|
for (const entry of entries) {
|
|
10168
10702
|
if (entry.name.startsWith(".")) continue;
|
|
10169
|
-
const child =
|
|
10703
|
+
const child = join19(dir, entry.name);
|
|
10170
10704
|
let isDirectory = entry.isDirectory();
|
|
10171
10705
|
let isFile = entry.isFile();
|
|
10172
10706
|
if (entry.isSymbolicLink()) {
|
|
10173
10707
|
let target;
|
|
10174
10708
|
try {
|
|
10175
|
-
target =
|
|
10709
|
+
target = statSync5(child);
|
|
10176
10710
|
} catch {
|
|
10177
10711
|
skipped.push({ path: child, reason: "broken symlink" });
|
|
10178
10712
|
continue;
|
|
@@ -10210,7 +10744,7 @@ function collectImportFiles(inputs, opts = {}) {
|
|
|
10210
10744
|
for (const input of inputs) {
|
|
10211
10745
|
let isDirectory;
|
|
10212
10746
|
try {
|
|
10213
|
-
isDirectory =
|
|
10747
|
+
isDirectory = statSync5(input).isDirectory();
|
|
10214
10748
|
} catch {
|
|
10215
10749
|
missing.push(input);
|
|
10216
10750
|
continue;
|
|
@@ -10226,7 +10760,7 @@ function buildImportPlan(collected) {
|
|
|
10226
10760
|
for (const path of collected.files) {
|
|
10227
10761
|
let text2;
|
|
10228
10762
|
try {
|
|
10229
|
-
text2 =
|
|
10763
|
+
text2 = readFileSync15(path, "utf8");
|
|
10230
10764
|
} catch (error) {
|
|
10231
10765
|
throw new Error(
|
|
10232
10766
|
`couldn't read ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -10327,7 +10861,7 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10327
10861
|
}
|
|
10328
10862
|
if (textOpt != null) return { text: textOpt, defaultTitle: null };
|
|
10329
10863
|
const fromStdin = fileOpt === "-";
|
|
10330
|
-
const text2 = fromStdin ?
|
|
10864
|
+
const text2 = fromStdin ? readFileSync16(0, "utf8") : readFileSync16(String(fileOpt), "utf8");
|
|
10331
10865
|
if (text2.trim().length === 0) {
|
|
10332
10866
|
fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
|
|
10333
10867
|
}
|
|
@@ -10979,16 +11513,16 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
|
|
|
10979
11513
|
}
|
|
10980
11514
|
|
|
10981
11515
|
// src/setup/apply.ts
|
|
10982
|
-
import { createHash as createHash5, randomUUID as
|
|
11516
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
10983
11517
|
import {
|
|
10984
11518
|
chmodSync as chmodSync2,
|
|
10985
11519
|
copyFileSync,
|
|
10986
11520
|
existsSync as existsSync13,
|
|
10987
11521
|
mkdirSync as mkdirSync16,
|
|
10988
|
-
readFileSync as
|
|
11522
|
+
readFileSync as readFileSync17,
|
|
10989
11523
|
renameSync as renameSync4,
|
|
10990
|
-
rmSync as
|
|
10991
|
-
statSync as
|
|
11524
|
+
rmSync as rmSync8,
|
|
11525
|
+
statSync as statSync6,
|
|
10992
11526
|
writeFileSync as writeFileSync15
|
|
10993
11527
|
} from "fs";
|
|
10994
11528
|
import { dirname as dirname15 } from "path";
|
|
@@ -11055,7 +11589,7 @@ function ensureDir2(path) {
|
|
|
11055
11589
|
}
|
|
11056
11590
|
function readOr(path, fallback) {
|
|
11057
11591
|
try {
|
|
11058
|
-
return
|
|
11592
|
+
return readFileSync17(path, "utf8");
|
|
11059
11593
|
} catch {
|
|
11060
11594
|
return fallback;
|
|
11061
11595
|
}
|
|
@@ -11066,7 +11600,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
11066
11600
|
let current = {};
|
|
11067
11601
|
if (existed) {
|
|
11068
11602
|
try {
|
|
11069
|
-
current = JSON.parse(
|
|
11603
|
+
current = JSON.parse(readFileSync17(path, "utf8"));
|
|
11070
11604
|
} catch {
|
|
11071
11605
|
return {
|
|
11072
11606
|
kind: "mcp",
|
|
@@ -11436,24 +11970,24 @@ var defaultTomlFileOperations = {
|
|
|
11436
11970
|
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11437
11971
|
ensureDir2(path);
|
|
11438
11972
|
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11439
|
-
const temporary = `${path}.sechroom-${process.pid}-${
|
|
11973
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
11440
11974
|
const backup = `${path}.bak`;
|
|
11441
|
-
const backupTemporary = `${backup}.sechroom-${process.pid}-${
|
|
11442
|
-
const mode = existsSync13(path) ?
|
|
11975
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID4()}.tmp`;
|
|
11976
|
+
const mode = existsSync13(path) ? statSync6(path).mode & 4095 : 384;
|
|
11443
11977
|
try {
|
|
11444
11978
|
writeFileSync15(temporary, content, { mode });
|
|
11445
11979
|
chmodSync2(temporary, mode);
|
|
11446
|
-
validateToml(
|
|
11980
|
+
validateToml(readFileSync17(temporary, "utf8"));
|
|
11447
11981
|
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11448
11982
|
operations.copyFileSync(path, backupTemporary);
|
|
11449
11983
|
chmodSync2(backupTemporary, mode);
|
|
11450
|
-
validateToml(
|
|
11984
|
+
validateToml(readFileSync17(backupTemporary, "utf8"));
|
|
11451
11985
|
operations.renameSync(backupTemporary, backup);
|
|
11452
11986
|
}
|
|
11453
11987
|
operations.renameSync(temporary, path);
|
|
11454
11988
|
} finally {
|
|
11455
|
-
|
|
11456
|
-
|
|
11989
|
+
rmSync8(temporary, { force: true });
|
|
11990
|
+
rmSync8(backupTemporary, { force: true });
|
|
11457
11991
|
}
|
|
11458
11992
|
}
|
|
11459
11993
|
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
@@ -11629,7 +12163,15 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
11629
12163
|
kind: "instruction",
|
|
11630
12164
|
path: target.instruction.path,
|
|
11631
12165
|
status: "skipped",
|
|
11632
|
-
|
|
12166
|
+
// Name the block even when nothing resolved. Without it the skip reports a
|
|
12167
|
+
// path and no identity, so a consumer cannot tell WHICH managed block went
|
|
12168
|
+
// missing — absent rather than loud, which is the failure this guards.
|
|
12169
|
+
block: "role-template",
|
|
12170
|
+
// GUARD — an unresolved template must NAME what it looked for. A bare
|
|
12171
|
+
// "not found" reads identically whether the bundle is absent or the memo
|
|
12172
|
+
// was retagged out of every candidate family, and the second case is a
|
|
12173
|
+
// silent loss of a template the installation previously had.
|
|
12174
|
+
note: "no role template resolved \u2014 tried " + attemptedTagSets(section).map((set) => `[${set.join(", ")}]`).join(" then ") + " \u2014 install the SEM Starter bundle, or check the template's target tag, then re-run `sechroom setup agent-files`"
|
|
11633
12175
|
});
|
|
11634
12176
|
} else {
|
|
11635
12177
|
const action = applyBlock(
|
|
@@ -11734,7 +12276,7 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
11734
12276
|
|
|
11735
12277
|
// src/setup/skills-offer.ts
|
|
11736
12278
|
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync16 } from "fs";
|
|
11737
|
-
import { join as
|
|
12279
|
+
import { join as join20 } from "path";
|
|
11738
12280
|
|
|
11739
12281
|
// src/setup/lane-pin.ts
|
|
11740
12282
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -11850,8 +12392,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
11850
12392
|
if (skills.length > 0) {
|
|
11851
12393
|
const written = [];
|
|
11852
12394
|
for (const s of skills) {
|
|
11853
|
-
mkdirSync17(
|
|
11854
|
-
writeFileSync16(
|
|
12395
|
+
mkdirSync17(join20(sDir, s.name), { recursive: true });
|
|
12396
|
+
writeFileSync16(join20(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
11855
12397
|
written.push(s.name);
|
|
11856
12398
|
}
|
|
11857
12399
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -11863,7 +12405,7 @@ Found ${summary} available to you for ${surface}.
|
|
|
11863
12405
|
const written = [];
|
|
11864
12406
|
for (const a of agents) {
|
|
11865
12407
|
const file = `${a.name}.md`;
|
|
11866
|
-
writeFileSync16(
|
|
12408
|
+
writeFileSync16(join20(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
11867
12409
|
written.push(file);
|
|
11868
12410
|
}
|
|
11869
12411
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -11976,8 +12518,17 @@ function buildCheckReport(result) {
|
|
|
11976
12518
|
stub: 0
|
|
11977
12519
|
};
|
|
11978
12520
|
const blocks = [];
|
|
12521
|
+
const unresolved = [];
|
|
11979
12522
|
for (const { client, actions } of result) {
|
|
11980
12523
|
for (const action of actions) {
|
|
12524
|
+
if (action.kind === "instruction" && action.status === "skipped" && !action.eval) {
|
|
12525
|
+
unresolved.push({
|
|
12526
|
+
client,
|
|
12527
|
+
path: action.path,
|
|
12528
|
+
block: action.block ?? "unknown",
|
|
12529
|
+
note: action.note
|
|
12530
|
+
});
|
|
12531
|
+
}
|
|
11981
12532
|
if (!action.eval) continue;
|
|
11982
12533
|
counts[action.eval]++;
|
|
11983
12534
|
blocks.push({
|
|
@@ -11991,7 +12542,8 @@ function buildCheckReport(result) {
|
|
|
11991
12542
|
return {
|
|
11992
12543
|
eval: counts,
|
|
11993
12544
|
wouldChange: counts.stale + counts.drift + counts.absent,
|
|
11994
|
-
blocks
|
|
12545
|
+
blocks,
|
|
12546
|
+
unresolved
|
|
11995
12547
|
};
|
|
11996
12548
|
}
|
|
11997
12549
|
function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
@@ -12006,7 +12558,7 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
|
12006
12558
|
},
|
|
12007
12559
|
true
|
|
12008
12560
|
);
|
|
12009
|
-
} else if (report.wouldChange === 0) {
|
|
12561
|
+
} else if (report.wouldChange === 0 && report.unresolved.length === 0) {
|
|
12010
12562
|
if (report.eval.stub) {
|
|
12011
12563
|
const files = report.eval.stub === 1 ? "file" : "files";
|
|
12012
12564
|
process.stdout.write(
|
|
@@ -12018,11 +12570,27 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
|
|
|
12018
12570
|
}
|
|
12019
12571
|
} else {
|
|
12020
12572
|
const bits = [];
|
|
12021
|
-
if (report.
|
|
12022
|
-
|
|
12023
|
-
|
|
12573
|
+
if (report.wouldChange > 0) {
|
|
12574
|
+
const changes = [];
|
|
12575
|
+
if (report.eval.stale) changes.push(`${report.eval.stale} out of date`);
|
|
12576
|
+
if (report.eval.drift)
|
|
12577
|
+
changes.push(`${report.eval.drift} with local edits`);
|
|
12578
|
+
if (report.eval.absent)
|
|
12579
|
+
changes.push(`${report.eval.absent} not yet written`);
|
|
12580
|
+
bits.push(
|
|
12581
|
+
`${report.wouldChange} instruction block(s) would change: ${changes.join(", ")}`
|
|
12582
|
+
);
|
|
12583
|
+
}
|
|
12584
|
+
if (report.unresolved.length > 0) {
|
|
12585
|
+
const details = report.unresolved.map(
|
|
12586
|
+
({ client, path, block, note }) => `${client} ${block} at ${path}${note ? ` \u2014 ${note}` : ""}`
|
|
12587
|
+
).join("; ");
|
|
12588
|
+
bits.push(
|
|
12589
|
+
`${report.unresolved.length} instruction template(s) skipped or unresolved: ${details}`
|
|
12590
|
+
);
|
|
12591
|
+
}
|
|
12024
12592
|
process.stderr.write(
|
|
12025
|
-
`\u26A0 ${
|
|
12593
|
+
`\u26A0 ${bits.join("; ")}. Run ${style.cyan(refreshCommand)}.
|
|
12026
12594
|
`
|
|
12027
12595
|
);
|
|
12028
12596
|
}
|
|
@@ -12099,7 +12667,7 @@ function registerInit(program2) {
|
|
|
12099
12667
|
false
|
|
12100
12668
|
).option(
|
|
12101
12669
|
"--check",
|
|
12102
|
-
"report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12670
|
+
"report whether agent files would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
12103
12671
|
false
|
|
12104
12672
|
).addHelpText(
|
|
12105
12673
|
"after",
|
|
@@ -12140,7 +12708,8 @@ Examples:
|
|
|
12140
12708
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12141
12709
|
const targets = clientTargets(process.cwd(), {
|
|
12142
12710
|
claudeDir: claudeTargets[0]?.dir,
|
|
12143
|
-
codexHome: codexHomes[0] ?? null
|
|
12711
|
+
codexHome: codexHomes[0] ?? null,
|
|
12712
|
+
codexScope: scope
|
|
12144
12713
|
});
|
|
12145
12714
|
const keys = resolveClientKeys(opts.client);
|
|
12146
12715
|
const json = g.json;
|
|
@@ -12242,7 +12811,7 @@ function registerSetup(program2, deps = {}) {
|
|
|
12242
12811
|
false
|
|
12243
12812
|
).option(
|
|
12244
12813
|
"--check",
|
|
12245
|
-
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12814
|
+
"report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
12246
12815
|
false
|
|
12247
12816
|
).addHelpText(
|
|
12248
12817
|
"after",
|
|
@@ -12432,15 +13001,30 @@ Examples:
|
|
|
12432
13001
|
"--client <list>",
|
|
12433
13002
|
`comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
12434
13003
|
DEFAULT_CLIENT_KEY
|
|
13004
|
+
).option(
|
|
13005
|
+
"--scope <scope>",
|
|
13006
|
+
"Codex MCP config scope: 'project' (<cwd>/.codex) or 'global' (CODEX_HOME / ~/.codex) \u2014 default project",
|
|
13007
|
+
"project"
|
|
12435
13008
|
).option("--dry-run", "print what would be written without writing", false).action(async (slug2, opts, cmd) => {
|
|
12436
13009
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
12437
13010
|
const setup = await withSpinner(
|
|
12438
13011
|
"Fetching setup descriptors",
|
|
12439
13012
|
() => fetchSetup(cfg, slug2)
|
|
12440
13013
|
);
|
|
12441
|
-
|
|
13014
|
+
let scope;
|
|
13015
|
+
try {
|
|
13016
|
+
scope = resolveScope(opts.scope);
|
|
13017
|
+
} catch (error) {
|
|
13018
|
+
return fail(error.message);
|
|
13019
|
+
}
|
|
13020
|
+
const globals = cmd.optsWithGlobals();
|
|
13021
|
+
const codexHome = scope === "global" ? resolveCodexHomes({ override: globals.codexHome, scope })[0] ?? null : null;
|
|
13022
|
+
const targets = clientTargets(process.cwd(), {
|
|
13023
|
+
codexHome,
|
|
13024
|
+
codexScope: scope
|
|
13025
|
+
});
|
|
12442
13026
|
const keys = resolveClientKeys(opts.client);
|
|
12443
|
-
const json =
|
|
13027
|
+
const json = globals.json;
|
|
12444
13028
|
const result = [];
|
|
12445
13029
|
for (const key of keys) {
|
|
12446
13030
|
const target = targets[key];
|
|
@@ -12467,12 +13051,12 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
|
|
|
12467
13051
|
|
|
12468
13052
|
// src/commands/onboard.ts
|
|
12469
13053
|
import { existsSync as existsSync15 } from "fs";
|
|
12470
|
-
import { basename as basename5, join as
|
|
13054
|
+
import { basename as basename5, join as join22 } from "path";
|
|
12471
13055
|
|
|
12472
13056
|
// src/commands/fanout.ts
|
|
12473
13057
|
import { spawnSync } from "child_process";
|
|
12474
|
-
import { existsSync as existsSync14, readFileSync as
|
|
12475
|
-
import { isAbsolute as isAbsolute3, join as
|
|
13058
|
+
import { existsSync as existsSync14, readFileSync as readFileSync18, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
13059
|
+
import { isAbsolute as isAbsolute3, join as join21, resolve as resolve7 } from "path";
|
|
12476
13060
|
var ICON = {
|
|
12477
13061
|
refresh: "\u21BB",
|
|
12478
13062
|
bind: "+",
|
|
@@ -12485,20 +13069,20 @@ function resolveChildDir(path, root) {
|
|
|
12485
13069
|
function discoverChildren(root) {
|
|
12486
13070
|
let names;
|
|
12487
13071
|
try {
|
|
12488
|
-
names =
|
|
13072
|
+
names = readdirSync5(root);
|
|
12489
13073
|
} catch {
|
|
12490
13074
|
return [];
|
|
12491
13075
|
}
|
|
12492
13076
|
const out = [];
|
|
12493
13077
|
for (const name of names.sort()) {
|
|
12494
13078
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
12495
|
-
const dir =
|
|
13079
|
+
const dir = join21(root, name);
|
|
12496
13080
|
try {
|
|
12497
|
-
if (!
|
|
13081
|
+
if (!statSync7(dir).isDirectory()) continue;
|
|
12498
13082
|
} catch {
|
|
12499
13083
|
continue;
|
|
12500
13084
|
}
|
|
12501
|
-
if (existsSync14(
|
|
13085
|
+
if (existsSync14(join21(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
12502
13086
|
}
|
|
12503
13087
|
return out;
|
|
12504
13088
|
}
|
|
@@ -12506,7 +13090,7 @@ function readManifest(path) {
|
|
|
12506
13090
|
if (!existsSync14(path)) return null;
|
|
12507
13091
|
let parsed;
|
|
12508
13092
|
try {
|
|
12509
|
-
parsed = JSON.parse(
|
|
13093
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
12510
13094
|
} catch (err2) {
|
|
12511
13095
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
12512
13096
|
}
|
|
@@ -12974,7 +13558,7 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
12974
13558
|
reason: "directory does not exist"
|
|
12975
13559
|
};
|
|
12976
13560
|
}
|
|
12977
|
-
if (existsSync15(
|
|
13561
|
+
if (existsSync15(join22(dir, ".sechroom.json"))) {
|
|
12978
13562
|
return {
|
|
12979
13563
|
label: entry.path,
|
|
12980
13564
|
dir,
|
|
@@ -13081,7 +13665,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
13081
13665
|
async function runRecurse(cfg, g, opts) {
|
|
13082
13666
|
const { yes, dryRun, json } = opts;
|
|
13083
13667
|
const root = process.cwd();
|
|
13084
|
-
const manifestPath =
|
|
13668
|
+
const manifestPath = join22(root, ".sechroom", "repos.json");
|
|
13085
13669
|
const fromManifest = readManifest(manifestPath);
|
|
13086
13670
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
13087
13671
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -13185,7 +13769,7 @@ function registerOnboard(program2) {
|
|
|
13185
13769
|
false
|
|
13186
13770
|
).option(
|
|
13187
13771
|
"--check",
|
|
13188
|
-
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
13772
|
+
"report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
|
|
13189
13773
|
false
|
|
13190
13774
|
).option(
|
|
13191
13775
|
"-y, --yes",
|
|
@@ -13330,20 +13914,15 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
13330
13914
|
yes,
|
|
13331
13915
|
process.cwd()
|
|
13332
13916
|
);
|
|
13333
|
-
const keys =
|
|
13334
|
-
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13335
|
-
process.stderr.write(
|
|
13336
|
-
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13337
|
-
`
|
|
13338
|
-
);
|
|
13339
|
-
}
|
|
13917
|
+
const keys = requestedKeys;
|
|
13340
13918
|
const setup = await withSpinner(
|
|
13341
13919
|
"Fetching setup descriptors",
|
|
13342
13920
|
() => fetchSetup(cfg)
|
|
13343
13921
|
);
|
|
13344
13922
|
const targets = clientTargets(process.cwd(), {
|
|
13345
13923
|
claudeDir: claudeTargets[0]?.dir,
|
|
13346
|
-
codexHome: codexHomes[0] ?? null
|
|
13924
|
+
codexHome: codexHomes[0] ?? null,
|
|
13925
|
+
codexScope: scope
|
|
13347
13926
|
});
|
|
13348
13927
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
13349
13928
|
if (!dryRun && !check) {
|
|
@@ -13873,31 +14452,31 @@ Examples:
|
|
|
13873
14452
|
|
|
13874
14453
|
// src/commands/reset.ts
|
|
13875
14454
|
import { homedir as homedir6 } from "os";
|
|
13876
|
-
import { join as
|
|
13877
|
-
import { existsSync as existsSync16, readFileSync as
|
|
14455
|
+
import { join as join23 } from "path";
|
|
14456
|
+
import { existsSync as existsSync16, readFileSync as readFileSync19, rmSync as rmSync9 } from "fs";
|
|
13878
14457
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
13879
|
-
var localSkillsDir = () =>
|
|
13880
|
-
var globalSkillsDir = () =>
|
|
13881
|
-
var localAgentsDir = () =>
|
|
13882
|
-
var globalAgentsDir = () =>
|
|
14458
|
+
var localSkillsDir = () => join23(process.cwd(), ".claude", "skills");
|
|
14459
|
+
var globalSkillsDir = () => join23(homedir6(), ".claude", "skills");
|
|
14460
|
+
var localAgentsDir = () => join23(process.cwd(), ".claude", "agents");
|
|
14461
|
+
var globalAgentsDir = () => join23(homedir6(), ".claude", "agents");
|
|
13883
14462
|
function removeMaterialisedSkills(dir) {
|
|
13884
14463
|
const removed = [];
|
|
13885
|
-
const lockPath =
|
|
14464
|
+
const lockPath = join23(dir, SKILLS_LOCK2);
|
|
13886
14465
|
if (!existsSync16(lockPath)) return removed;
|
|
13887
14466
|
try {
|
|
13888
|
-
const lock = JSON.parse(
|
|
14467
|
+
const lock = JSON.parse(readFileSync19(lockPath, "utf8"));
|
|
13889
14468
|
for (const entry of Object.values(lock)) {
|
|
13890
14469
|
for (const name of entry.skills ?? []) {
|
|
13891
|
-
const p =
|
|
14470
|
+
const p = join23(dir, name);
|
|
13892
14471
|
if (existsSync16(p)) {
|
|
13893
|
-
|
|
14472
|
+
rmSync9(p, { recursive: true, force: true });
|
|
13894
14473
|
removed.push(p);
|
|
13895
14474
|
}
|
|
13896
14475
|
}
|
|
13897
14476
|
}
|
|
13898
14477
|
} catch {
|
|
13899
14478
|
}
|
|
13900
|
-
|
|
14479
|
+
rmSync9(lockPath, { force: true });
|
|
13901
14480
|
removed.push(lockPath);
|
|
13902
14481
|
return removed;
|
|
13903
14482
|
}
|
|
@@ -13934,19 +14513,19 @@ function registerReset(program2) {
|
|
|
13934
14513
|
}
|
|
13935
14514
|
}
|
|
13936
14515
|
const removed = [];
|
|
13937
|
-
const stateDir =
|
|
14516
|
+
const stateDir = join23(process.cwd(), ".sechroom");
|
|
13938
14517
|
if (existsSync16(stateDir)) {
|
|
13939
|
-
|
|
14518
|
+
rmSync9(stateDir, { recursive: true, force: true });
|
|
13940
14519
|
removed.push(stateDir);
|
|
13941
14520
|
}
|
|
13942
|
-
const legacyCfg =
|
|
14521
|
+
const legacyCfg = join23(process.cwd(), ".sechroom.json");
|
|
13943
14522
|
if (existsSync16(legacyCfg)) {
|
|
13944
|
-
|
|
14523
|
+
rmSync9(legacyCfg, { force: true });
|
|
13945
14524
|
removed.push(legacyCfg);
|
|
13946
14525
|
}
|
|
13947
|
-
const legacySem =
|
|
14526
|
+
const legacySem = join23(process.cwd(), ".sem");
|
|
13948
14527
|
if (existsSync16(legacySem)) {
|
|
13949
|
-
|
|
14528
|
+
rmSync9(legacySem, { force: true });
|
|
13950
14529
|
removed.push(legacySem);
|
|
13951
14530
|
}
|
|
13952
14531
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -13971,8 +14550,8 @@ function registerReset(program2) {
|
|
|
13971
14550
|
}
|
|
13972
14551
|
|
|
13973
14552
|
// src/commands/skills.ts
|
|
13974
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
13975
|
-
import { join as
|
|
14553
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
|
|
14554
|
+
import { join as join24 } from "path";
|
|
13976
14555
|
function filenameFromDisposition(header) {
|
|
13977
14556
|
if (!header) return void 0;
|
|
13978
14557
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -13980,11 +14559,11 @@ function filenameFromDisposition(header) {
|
|
|
13980
14559
|
}
|
|
13981
14560
|
function resolveOutputPath(output, serverFilename) {
|
|
13982
14561
|
const filename = serverFilename || "skills.zip";
|
|
13983
|
-
if (!output) return
|
|
13984
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
14562
|
+
if (!output) return join24(process.cwd(), filename);
|
|
14563
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync8(output).isDirectory();
|
|
13985
14564
|
if (looksLikeDir) {
|
|
13986
14565
|
mkdirSync18(output, { recursive: true });
|
|
13987
|
-
return
|
|
14566
|
+
return join24(output, filename);
|
|
13988
14567
|
}
|
|
13989
14568
|
return output;
|
|
13990
14569
|
}
|
|
@@ -14030,6 +14609,7 @@ Examples:
|
|
|
14030
14609
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
14031
14610
|
$ sechroom skills list what's materialised on disk
|
|
14032
14611
|
$ sechroom skills clean remove the materialised skill files
|
|
14612
|
+
$ sechroom skills clean --prune-orphans also sweep skills orphaned by an earlier rename
|
|
14033
14613
|
$ sechroom skills preview --workspace wsp_abc render a draft bundle from source (no install)
|
|
14034
14614
|
$ sechroom skills package my-bundle download the installed bundle's skills as a zip
|
|
14035
14615
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
@@ -14043,7 +14623,7 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14043
14623
|
);
|
|
14044
14624
|
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));
|
|
14045
14625
|
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));
|
|
14046
|
-
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));
|
|
14626
|
+
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("--prune-orphans", "also remove sechroom-installed skill dirs that no lock entry claims (renamed/removed upstream)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
14047
14627
|
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) => {
|
|
14048
14628
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
14049
14629
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -14190,8 +14770,8 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
14190
14770
|
|
|
14191
14771
|
// src/commands/sweep.ts
|
|
14192
14772
|
import { existsSync as existsSync18 } from "fs";
|
|
14193
|
-
import { dirname as dirname16, join as
|
|
14194
|
-
var DEFAULT_MANIFEST =
|
|
14773
|
+
import { dirname as dirname16, join as join25, resolve as resolve8 } from "path";
|
|
14774
|
+
var DEFAULT_MANIFEST = join25(".sechroom", "repos.json");
|
|
14195
14775
|
function planEntry(entry, root) {
|
|
14196
14776
|
const dir = resolveChildDir(entry.path, root);
|
|
14197
14777
|
if (!existsSync18(dir)) {
|
|
@@ -14695,7 +15275,7 @@ async function readStdin4() {
|
|
|
14695
15275
|
function resolveVersion() {
|
|
14696
15276
|
try {
|
|
14697
15277
|
const pkg = JSON.parse(
|
|
14698
|
-
|
|
15278
|
+
readFileSync20(new URL("../package.json", import.meta.url), "utf8")
|
|
14699
15279
|
);
|
|
14700
15280
|
return pkg.version ?? "0.0.0";
|
|
14701
15281
|
} catch {
|
|
@@ -14859,6 +15439,7 @@ registerWorkPlan(program);
|
|
|
14859
15439
|
registerExecutor(program);
|
|
14860
15440
|
registerClose(program);
|
|
14861
15441
|
registerFiling(program);
|
|
15442
|
+
registerGitHub(program);
|
|
14862
15443
|
registerContinuity(program);
|
|
14863
15444
|
registerCheckpoint(program);
|
|
14864
15445
|
registerHook(program);
|