@sechroom/cli 2026.8.4-rc.e38fd260b → 2026.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1836 -377
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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 (typeof error === "object" && error !== null && "title" in error) {
702
+ } else if (isRecord(error)) {
692
703
  const problem = error;
693
- msg = String(problem.title);
694
- if (problem.errors && typeof problem.errors === "object") {
695
- const detail = Object.entries(problem.errors).flatMap(
696
- ([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
697
- );
698
- if (detail.length > 0) msg += `
699
- ${detail.join("\n")}`;
704
+ const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
705
+ const problemDetail = 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
+ ...problemDetail ? [problemDetail] : [],
711
+ ...fieldErrors,
712
+ ...structuredErrors
713
+ ];
714
+ if (parts.length > 0) {
715
+ if (title && !problemDetail && 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}
@@ -1027,14 +1072,19 @@ async function fetchSetup(cfg, namespaceSlug) {
1027
1072
  "/operator-surface/setup",
1028
1073
  hasQuery ? { params: { query } } : {}
1029
1074
  );
1030
- if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(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((r) => typeof r.slug === "string").map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
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 parseTagArtifactId(id) {
1103
+ function parseTagArtifactCandidates(id) {
1054
1104
  if (!id.startsWith("tag:")) return null;
1055
- const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1056
- return tags.length > 0 ? tags : null;
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}", { params: { path: { memoryId: id } } });
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 { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
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 resolveInstruction(cfg, section, personalWorkspaceId) {
1073
- const client = await makeClient(cfg);
1074
- for (const artifact of section.artifacts) {
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: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
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
- const templateId = hits[0].id;
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) continue;
1085
- const templateTags = template.tags ?? tags;
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: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
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 { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
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 { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
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(`<!-- @sechroom/cli:section source=${ref} -->
1111
- ${mem.text.trim()}`);
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("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
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) throw new Error(`creating personal copy failed: ${JSON.stringify(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
@@ -4366,9 +4489,19 @@ import { dirname as dirname5, join as join7 } from "path";
4366
4489
  function claudeDesktopConfigPath(home) {
4367
4490
  switch (process.platform) {
4368
4491
  case "darwin":
4369
- return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4492
+ return join7(
4493
+ home,
4494
+ "Library",
4495
+ "Application Support",
4496
+ "Claude",
4497
+ "claude_desktop_config.json"
4498
+ );
4370
4499
  case "win32":
4371
- return join7(process.env.APPDATA ?? join7(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
4500
+ return join7(
4501
+ process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
4502
+ "Claude",
4503
+ "claude_desktop_config.json"
4504
+ );
4372
4505
  default:
4373
4506
  return join7(home, ".config", "Claude", "claude_desktop_config.json");
4374
4507
  }
@@ -4376,30 +4509,53 @@ function claudeDesktopConfigPath(home) {
4376
4509
  function clientTargets(cwd, opts = {}) {
4377
4510
  const home = homedir3();
4378
4511
  const claudeDir = opts.claudeDir ?? join7(home, ".claude");
4379
- const codexHome = opts.codexHome ?? join7(home, ".codex");
4512
+ const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
4380
4513
  return {
4381
4514
  "claude-code": {
4382
4515
  key: "claude-code",
4383
4516
  label: "Claude Code",
4384
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".mcp.json"), format: "json" },
4517
+ mcp: {
4518
+ surfaceKey: "claude-code",
4519
+ sectionType: SectionType.McpConfig,
4520
+ path: join7(cwd, ".mcp.json"),
4521
+ format: "json"
4522
+ },
4385
4523
  instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
4386
4524
  },
4387
4525
  "claude-desktop": {
4388
4526
  key: "claude-desktop",
4389
4527
  label: "Claude Desktop",
4390
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
4391
- instruction: { surfaceKey: "claude-desktop", path: join7(claudeDir, "CLAUDE.md") }
4528
+ mcp: {
4529
+ surfaceKey: "claude-desktop",
4530
+ sectionType: SectionType.McpConfig,
4531
+ path: claudeDesktopConfigPath(home),
4532
+ format: "json"
4533
+ },
4534
+ instruction: {
4535
+ surfaceKey: "claude-desktop",
4536
+ path: join7(claudeDir, "CLAUDE.md")
4537
+ }
4392
4538
  },
4393
4539
  codex: {
4394
4540
  key: "codex",
4395
4541
  label: "Codex CLI",
4396
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join7(codexHome, "config.toml"), format: "toml" },
4542
+ mcp: codexHome ? {
4543
+ surfaceKey: "chatgpt",
4544
+ sectionType: SectionType.McpConfigToml,
4545
+ path: join7(codexHome, "config.toml"),
4546
+ format: "toml"
4547
+ } : null,
4397
4548
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4398
4549
  },
4399
4550
  cursor: {
4400
4551
  key: "cursor",
4401
4552
  label: "Cursor",
4402
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".cursor", "mcp.json"), format: "json" },
4553
+ mcp: {
4554
+ surfaceKey: "claude-code",
4555
+ sectionType: SectionType.McpConfig,
4556
+ path: join7(cwd, ".cursor", "mcp.json"),
4557
+ format: "json"
4558
+ },
4403
4559
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4404
4560
  },
4405
4561
  antigravity: {
@@ -4411,20 +4567,34 @@ function clientTargets(cwd, opts = {}) {
4411
4567
  // `type` — comes from the `antigravity` server surface, so we don't
4412
4568
  // hardcode it here. Instructions go in the project `AGENTS.md`
4413
4569
  // (cross-tool, shared with Codex/Cursor).
4414
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join7(home, ".gemini", "config", "mcp_config.json"), format: "json" },
4570
+ mcp: {
4571
+ surfaceKey: "antigravity",
4572
+ sectionType: SectionType.McpConfig,
4573
+ path: join7(home, ".gemini", "config", "mcp_config.json"),
4574
+ format: "json"
4575
+ },
4415
4576
  instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
4416
4577
  }
4417
4578
  };
4418
4579
  }
4419
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
4580
+ var ALL_CLIENT_KEYS = [
4581
+ "claude-code",
4582
+ "claude-desktop",
4583
+ "codex",
4584
+ "cursor",
4585
+ "antigravity"
4586
+ ];
4420
4587
  var DEFAULT_CLIENT_KEY = "claude-code";
4421
4588
  function detectInstalledClients(cwd) {
4422
4589
  const home = homedir3();
4423
4590
  const detected = [];
4424
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
4425
- if (existsSync5(dirname5(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
4591
+ if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
4592
+ detected.push("claude-code");
4593
+ if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
4594
+ detected.push("claude-desktop");
4426
4595
  if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
4427
- if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor"))) detected.push("cursor");
4596
+ if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
4597
+ detected.push("cursor");
4428
4598
  if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
4429
4599
  return detected;
4430
4600
  }
@@ -5751,6 +5921,7 @@ Call sechroom_lifecycle_signal at each phase boundary (start/work/verify/closeou
5751
5921
  }
5752
5922
 
5753
5923
  // src/commands/executor.ts
5924
+ var DEFAULT_CLAIM_POLICY = "restricted";
5754
5925
  function executorSubscriptionInput(name) {
5755
5926
  return {
5756
5927
  name,
@@ -5812,8 +5983,8 @@ function registerExecutor(program2) {
5812
5983
  "Capability operation keys claimed by this instance"
5813
5984
  ).option(
5814
5985
  "--claim-policy <policy>",
5815
- "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
5816
- "open"
5986
+ "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
5987
+ DEFAULT_CLAIM_POLICY
5817
5988
  ).option(
5818
5989
  "--claim-tag <tag...>",
5819
5990
  "Task tag this instance accepts under --claim-policy restricted"
@@ -5828,7 +5999,12 @@ function registerExecutor(program2) {
5828
5999
  "--subscription-name <name>",
5829
6000
  "SignalR delivery binding name",
5830
6001
  "executor-dispatch"
5831
- ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option(
6002
+ ).option(
6003
+ "--ttl <seconds>",
6004
+ "Advertisement TTL (30 to tenant maximum)",
6005
+ parseInteger,
6006
+ 600
6007
+ ).option(
5832
6008
  "--task-lease-ttl <seconds>",
5833
6009
  "Task lease TTL (60-86400)",
5834
6010
  parseInteger,
@@ -5915,7 +6091,7 @@ function registerExecutor(program2) {
5915
6091
  taskLeaseTtlSeconds: opts.taskLeaseTtl,
5916
6092
  modelId: opts.modelId,
5917
6093
  effortLabel: opts.effortLabel,
5918
- claimPolicy: (opts.claimPolicy ?? "open").toLowerCase() === "restricted" ? "restricted" : "open",
6094
+ claimPolicy: parseClaimPolicy(opts.claimPolicy) === "Restricted" ? "restricted" : "open",
5919
6095
  claimTags: opts.claimTag ?? [],
5920
6096
  excludeTags: opts.excludeTag ?? [],
5921
6097
  relayId: opts.relay,
@@ -6036,8 +6212,8 @@ function registerExecutor(program2) {
6036
6212
  "Capability operation keys claimed by this instance"
6037
6213
  ).option(
6038
6214
  "--claim-policy <policy>",
6039
- "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
6040
- "open"
6215
+ "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
6216
+ DEFAULT_CLAIM_POLICY
6041
6217
  ).option(
6042
6218
  "--claim-tag <tag...>",
6043
6219
  "Task tag this instance accepts under --claim-policy restricted"
@@ -6048,7 +6224,12 @@ function registerExecutor(program2) {
6048
6224
  "--activation-mode <mode>",
6049
6225
  "attached | detached \u2014 detached marks a fleet run as a service that outlives its shell (default attached)",
6050
6226
  "attached"
6051
- ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option(
6227
+ ).option(
6228
+ "--ttl <seconds>",
6229
+ "Advertisement TTL (30 to tenant maximum)",
6230
+ parseInteger,
6231
+ 120
6232
+ ).option(
6052
6233
  "--task-lease-ttl <seconds>",
6053
6234
  "Task lease TTL (60-86400)",
6054
6235
  parseInteger,
@@ -6104,7 +6285,12 @@ function registerExecutor(program2) {
6104
6285
  `)
6105
6286
  );
6106
6287
  });
6107
- executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
6288
+ executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
6289
+ "--ttl <seconds>",
6290
+ "Advertisement TTL (30 to tenant maximum)",
6291
+ parseInteger,
6292
+ 120
6293
+ ).action(async (id, opts, cmd) => {
6108
6294
  const data = await refreshExecutorInstance(
6109
6295
  resolveConfig(cmd.optsWithGlobals()),
6110
6296
  id,
@@ -6112,7 +6298,12 @@ function registerExecutor(program2) {
6112
6298
  );
6113
6299
  emit(data, Boolean(cmd.optsWithGlobals().json));
6114
6300
  });
6115
- executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
6301
+ executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
6302
+ "--ttl <seconds>",
6303
+ "Advertisement TTL (30 to tenant maximum)",
6304
+ parseInteger,
6305
+ 120
6306
+ ).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
6116
6307
  if (opts.interval >= opts.ttl)
6117
6308
  fail("heartbeat interval must be shorter than the TTL");
6118
6309
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -6274,12 +6465,12 @@ function parseRuntimeKind(value) {
6274
6465
  }
6275
6466
  }
6276
6467
  function parseClaimPolicy(value) {
6277
- switch ((value ?? "open").trim().toLowerCase()) {
6468
+ switch ((value ?? DEFAULT_CLAIM_POLICY).trim().toLowerCase()) {
6278
6469
  case "":
6279
- case "open":
6280
- return "Open";
6281
6470
  case "restricted":
6282
6471
  return "Restricted";
6472
+ case "open":
6473
+ return "Open";
6283
6474
  default:
6284
6475
  return fail("claim-policy must be open or restricted");
6285
6476
  }
@@ -7607,6 +7798,7 @@ function resolveLane(flagLane, cwd) {
7607
7798
  return applyWorktreeLaneSuffix(base, start);
7608
7799
  }
7609
7800
  var INTENT_FILE = join15(".sechroom", "continuity.json");
7801
+ var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
7610
7802
  function resolveIntentPath(start) {
7611
7803
  let dir = start;
7612
7804
  for (; ; ) {
@@ -7631,6 +7823,16 @@ function hasRequiredIntent(i) {
7631
7823
  i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
7632
7824
  );
7633
7825
  }
7826
+ function localDryRunMissingFields(i) {
7827
+ const required = [
7828
+ ["objective", "--objective"],
7829
+ ["state", "--state"],
7830
+ ["lastAction", "--last-action"],
7831
+ ["nextAction", "--next-action"],
7832
+ ["resumeInstruction", "--resume-instruction"]
7833
+ ];
7834
+ return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
7835
+ }
7634
7836
  async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
7635
7837
  const lane = resolveLane(laneFlag, cwd);
7636
7838
  if (!lane) return false;
@@ -7652,7 +7854,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
7652
7854
  openQuestions: intent.questions ?? null,
7653
7855
  surfaceMarkers: intent.surfaceMarkers ?? null,
7654
7856
  relevantArtifactIds: intent.artifacts ?? null,
7655
- confidence: intent.confidence ?? null,
7857
+ // Preserve invalid JSON-string confidence tokens for the server's semantic
7858
+ // validator; never coerce them through Number/NaN/null.
7859
+ confidence: confidenceWireValue(intent.confidence),
7656
7860
  // Frequent triggers (compaction, session-end) land within the FR-051 4h
7657
7861
  // window; Acknowledge lets the checkpoint persist on the lane.
7658
7862
  concurrentSessionPolicy: "Acknowledge"
@@ -7947,12 +8151,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
7947
8151
  function registerCheckpoint(program2) {
7948
8152
  program2.command("checkpoint").description(
7949
8153
  "Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
7950
- ).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("--dry-run", "validate + print the snapshot payload without creating it or writing the file", false).addHelpText(
8154
+ ).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(
8155
+ "--dry-run",
8156
+ "run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
8157
+ false
8158
+ ).addHelpText(
7951
8159
  "after",
7952
8160
  `
7953
8161
  File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
7954
8162
  overrides that field. The snapshot is created FIRST (server-validated), then the local file is
7955
8163
  written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
8164
+ --dry-run performs only the local required-field check and prints a payload. Its output is
8165
+ explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
7956
8166
 
7957
8167
  Examples:
7958
8168
  $ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
@@ -7975,7 +8185,9 @@ Examples:
7975
8185
  questions: opts.question ?? base.questions,
7976
8186
  surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
7977
8187
  artifacts: opts.artifact ?? base.artifacts,
7978
- confidence: opts.confidence != null ? Number(opts.confidence) : base.confidence
8188
+ // Keep the raw token until the server's shared validator sees it. In particular,
8189
+ // Number("high") -> NaN -> JSON null would silently discard the operator's input.
8190
+ confidence: opts.confidence != null ? opts.confidence : base.confidence
7979
8191
  };
7980
8192
  const lane = resolveLane(opts.lane, cwd);
7981
8193
  if (!lane) {
@@ -7983,19 +8195,6 @@ Examples:
7983
8195
  "no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
7984
8196
  );
7985
8197
  }
7986
- const required = [
7987
- ["objective", "--objective"],
7988
- ["state", "--state"],
7989
- ["lastAction", "--last-action"],
7990
- ["nextAction", "--next-action"],
7991
- ["resumeInstruction", "--resume-instruction"]
7992
- ];
7993
- const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
7994
- if (missing.length > 0) {
7995
- fail(
7996
- `missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
7997
- );
7998
- }
7999
8198
  const scope = merged.scope ?? "session";
8000
8199
  const body = {
8001
8200
  laneId: lane,
@@ -8009,13 +8208,33 @@ Examples:
8009
8208
  openQuestions: merged.questions ?? null,
8010
8209
  surfaceMarkers: merged.surfaceMarkers ?? null,
8011
8210
  relevantArtifactIds: merged.artifacts ?? null,
8012
- confidence: merged.confidence ?? null,
8211
+ confidence: confidenceWireValue(merged.confidence),
8013
8212
  // Explicit checkpoints are often within the FR-051 4h window; Acknowledge
8014
8213
  // lets one land on the lane (matches `hook pre-compact`).
8015
8214
  concurrentSessionPolicy: "Acknowledge"
8016
8215
  };
8017
8216
  if (opts.dryRun) {
8018
- emit({ dryRun: true, lane, scope, wouldCreate: body }, json);
8217
+ const missing = localDryRunMissingFields(merged);
8218
+ if (missing.length > 0) {
8219
+ fail(
8220
+ `LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
8221
+ );
8222
+ }
8223
+ emit(
8224
+ {
8225
+ dryRun: true,
8226
+ validation: {
8227
+ mode: "local-only",
8228
+ serverValidated: false,
8229
+ checks: ["required-fields"],
8230
+ warning: LOCAL_DRY_RUN_VALIDATION_WARNING
8231
+ },
8232
+ lane,
8233
+ scope,
8234
+ wouldCreate: body
8235
+ },
8236
+ json
8237
+ );
8019
8238
  return;
8020
8239
  }
8021
8240
  const data = await runApi("Creating snapshot", async () => {
@@ -8252,7 +8471,7 @@ Displaced snapshot recovery surfaces:
8252
8471
  openQuestions: opts.question ?? null,
8253
8472
  surfaceMarkers: opts.surfaceMarker ?? null,
8254
8473
  relevantArtifactIds: opts.artifact ?? null,
8255
- confidence: opts.confidence != null ? Number(opts.confidence) : null
8474
+ confidence: confidenceWireValue(opts.confidence)
8256
8475
  }
8257
8476
  });
8258
8477
  });
@@ -8401,6 +8620,96 @@ function snapshotGetNotFoundHint(includeAll, status) {
8401
8620
 
8402
8621
  // src/commands/work-plan.ts
8403
8622
  import { readFile as readFile2 } from "fs/promises";
8623
+
8624
+ // src/paging.ts
8625
+ var MAX_AUTO_PAGES = 100;
8626
+ var asNumber = (value) => value === void 0 || value === "" ? void 0 : Number(value);
8627
+ function shouldAutoPage(opts) {
8628
+ if (opts.autoPage === false) return false;
8629
+ return asNumber(opts.page) === void 0;
8630
+ }
8631
+ function singlePageQuery(opts) {
8632
+ const page = asNumber(opts.page);
8633
+ const pageSize = asNumber(opts.pageSize);
8634
+ return {
8635
+ ...page === void 0 ? {} : { page },
8636
+ ...pageSize === void 0 ? {} : { pageSize }
8637
+ };
8638
+ }
8639
+ async function fetchAllPages(fetchPage, opts = {}, label = "results") {
8640
+ const pages = [];
8641
+ let page = 1;
8642
+ let truncated = false;
8643
+ for (; ; ) {
8644
+ const current = await fetchPage(page);
8645
+ pages.push(current);
8646
+ if (!hasNextPage(current)) break;
8647
+ if (pages.length >= MAX_AUTO_PAGES) {
8648
+ truncated = true;
8649
+ break;
8650
+ }
8651
+ page = current.page + 1;
8652
+ }
8653
+ const first = pages[0];
8654
+ if (!first) throw new Error("paged read returned no envelope at all");
8655
+ if (truncated) {
8656
+ if (!isQuiet()) {
8657
+ process.stderr.write(
8658
+ `${warn("!")} Stopped after ${MAX_AUTO_PAGES} pages \u2014 more ${label} remain. ${style.dim("Narrow the filters, or read a specific window with --page/--page-size.")}
8659
+ `
8660
+ );
8661
+ }
8662
+ const partial = pages.flatMap((p) => p.items);
8663
+ return {
8664
+ ...first,
8665
+ items: partial,
8666
+ page: 1,
8667
+ pageSize: partial.length,
8668
+ // Pages OF THIS SIZE, so the number stays consistent with the pageSize just reported.
8669
+ pageCount: partial.length === 0 ? 1 : Math.ceil(first.count / partial.length),
8670
+ hasPreviousPage: false,
8671
+ hasNextPage: true,
8672
+ isFirstPage: true,
8673
+ isLastPage: false,
8674
+ firstItemOnPage: partial.length === 0 ? 0 : 1,
8675
+ lastItemOnPage: partial.length
8676
+ };
8677
+ }
8678
+ if (pages.length === 1) return first;
8679
+ const items = pages.flatMap((current) => current.items);
8680
+ return {
8681
+ ...first,
8682
+ items,
8683
+ page: 1,
8684
+ pageSize: items.length,
8685
+ pageCount: 1,
8686
+ hasPreviousPage: false,
8687
+ hasNextPage: false,
8688
+ isFirstPage: true,
8689
+ isLastPage: true,
8690
+ firstItemOnPage: items.length === 0 ? 0 : 1,
8691
+ lastItemOnPage: items.length
8692
+ };
8693
+ }
8694
+ function hasNextPage(current) {
8695
+ if (current.hasNextPage !== void 0) return current.hasNextPage;
8696
+ if (current.pageCount !== void 0) return current.page < current.pageCount;
8697
+ return current.page * current.pageSize < current.count;
8698
+ }
8699
+ var PAGE_OPTION = [
8700
+ "--page <n>",
8701
+ "Page number (reads only that page instead of all)"
8702
+ ];
8703
+ var PAGE_SIZE_OPTION = [
8704
+ "--page-size <n>",
8705
+ "Page size used while paging"
8706
+ ];
8707
+ var NO_AUTO_PAGE_OPTION = [
8708
+ "--no-auto-page",
8709
+ "Read only the first page instead of walking every page"
8710
+ ];
8711
+
8712
+ // src/commands/work-plan.ts
8404
8713
  function registerWorkPlan(program2) {
8405
8714
  const workPlan = program2.command("work-plan").description(
8406
8715
  "Drive a work plan: create one from a brief, then execute / accept / reject"
@@ -8594,22 +8903,27 @@ Examples:
8594
8903
  globals.json
8595
8904
  );
8596
8905
  });
8597
- workPlan.command("list").description("List work plans, newest-first (GET /decompositions)").option("--status <status>", "Filter by decomposition status").option("--brief <briefId>", "Filter by work-brief memory id").option("--page <n>", "1-based page number", (v) => Number.parseInt(v, 10)).option("--page-size <n>", "Page size", (v) => Number.parseInt(v, 10)).action(async (opts, cmd) => {
8906
+ workPlan.command("list").description(
8907
+ "List work plans, newest-first (GET /decompositions). Walks every page by default."
8908
+ ).option("--status <status>", "Filter by decomposition status").option("--brief <briefId>", "Filter by work-brief memory id").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
8598
8909
  const globals = cmd.optsWithGlobals();
8599
8910
  const cfg = resolveConfig(globals);
8600
- const data = await runApi("Listing work plans", async () => {
8911
+ const readPage = (query) => runApi("Listing work plans", async () => {
8601
8912
  const client = await makeClient(cfg);
8602
8913
  return client.GET("/decompositions", {
8603
8914
  params: {
8604
- query: {
8605
- status: opts.status,
8606
- briefId: opts.brief,
8607
- page: opts.page,
8608
- pageSize: opts.pageSize
8609
- }
8915
+ query: { status: opts.status, briefId: opts.brief, ...query }
8610
8916
  }
8611
8917
  });
8612
8918
  });
8919
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
8920
+ (page) => readPage({
8921
+ page,
8922
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
8923
+ }),
8924
+ opts,
8925
+ "work plans"
8926
+ ) : await readPage(singlePageQuery(opts));
8613
8927
  emitAction(
8614
8928
  `listed ${style.bold(String(data.items.length))} of ${data.count} work plan(s)`,
8615
8929
  data,
@@ -8697,35 +9011,53 @@ Examples:
8697
9011
  $ sechroom filing reject fsg_XXXX --reason "wrong workspace"
8698
9012
  $ sechroom filing edit-and-accept fsg_XXXX --target-kind Workspace --existing-target-id wsp_XXXX`
8699
9013
  );
8700
- filing.command("suggestions").description("List filing suggestions (GET /filing/suggestions)").option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
9014
+ filing.command("suggestions").description(
9015
+ "List filing suggestions (GET /filing/suggestions). Walks every page by default."
9016
+ ).option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
8701
9017
  "--status <status>",
8702
9018
  "Generating | Pending | Accepted | Rejected | EditedAndAccepted | Deferred | Invalidated"
8703
- ).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
9019
+ ).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
8704
9020
  const cfg = resolveConfig(cmd.optsWithGlobals());
8705
- const data = await runApi("Listing filing suggestions", async () => {
9021
+ const filters = {
9022
+ ...opts.memoryId ? { memoryId: opts.memoryId } : {},
9023
+ ...opts.status ? {
9024
+ status: opts.status
9025
+ } : {}
9026
+ };
9027
+ const readPage = (query) => runApi("Listing filing suggestions", async () => {
8706
9028
  const client = await makeClient(cfg);
8707
9029
  return client.GET("/filing/suggestions", {
8708
- params: {
8709
- query: {
8710
- ...opts.memoryId ? { memoryId: opts.memoryId } : {},
8711
- ...opts.status ? { status: opts.status } : {},
8712
- ...opts.page ? { page: Number(opts.page) } : {},
8713
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
8714
- }
8715
- }
9030
+ params: { query: { ...filters, ...query } }
8716
9031
  });
8717
9032
  });
9033
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
9034
+ (page) => readPage({
9035
+ page,
9036
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
9037
+ }),
9038
+ opts,
9039
+ "filing suggestions"
9040
+ ) : await readPage(singlePageQuery(opts));
8718
9041
  emit(data, cmd.optsWithGlobals().json);
8719
9042
  });
8720
- filing.command("get <id>").description("Fetch a filing suggestion by id (GET /filing/suggestions/{id})").action(async (id, _opts, cmd) => {
9043
+ filing.command("get <id>").description(
9044
+ "Fetch a filing suggestion by id (GET /filing/suggestions/{id})"
9045
+ ).action(async (id, _opts, cmd) => {
8721
9046
  const cfg = resolveConfig(cmd.optsWithGlobals());
8722
9047
  const data = await runApi("Fetching filing suggestion", async () => {
8723
9048
  const client = await makeClient(cfg);
8724
- return client.GET("/filing/suggestions/{id}", { params: { path: { id } } });
9049
+ return client.GET("/filing/suggestions/{id}", {
9050
+ params: { path: { id } }
9051
+ });
8725
9052
  });
8726
9053
  emit(data, cmd.optsWithGlobals().json);
8727
9054
  });
8728
- filing.command("preview").description("Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)").option("--memory-id <memoryId>", "Preview filing for an existing memory").option("--text <text>", "Ad-hoc memory body text (instead of --memory-id)").option("--title <title>", "Ad-hoc memory title").option("--tag <tag...>", "Ad-hoc memory tags (repeatable)").option("--type <type>", "Ad-hoc memory type", "reference").option("--scope-workspace <workspaceId>", "Scope the preview to a workspace").action(async (opts, cmd) => {
9055
+ filing.command("preview").description(
9056
+ "Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)"
9057
+ ).option("--memory-id <memoryId>", "Preview filing for an existing memory").option("--text <text>", "Ad-hoc memory body text (instead of --memory-id)").option("--title <title>", "Ad-hoc memory title").option("--tag <tag...>", "Ad-hoc memory tags (repeatable)").option("--type <type>", "Ad-hoc memory type", "reference").option(
9058
+ "--scope-workspace <workspaceId>",
9059
+ "Scope the preview to a workspace"
9060
+ ).action(async (opts, cmd) => {
8729
9061
  const cfg = resolveConfig(cmd.optsWithGlobals());
8730
9062
  const memory = opts.text ? {
8731
9063
  text: opts.text,
@@ -8745,15 +9077,26 @@ Examples:
8745
9077
  });
8746
9078
  emit(data, cmd.optsWithGlobals().json);
8747
9079
  });
8748
- filing.command("accept <id>").description("Accept a filing suggestion (POST /filing/suggestions/{id}/accept)").action(async (id, _opts, cmd) => {
9080
+ filing.command("accept <id>").description(
9081
+ "Accept a filing suggestion (POST /filing/suggestions/{id}/accept)"
9082
+ ).action(async (id, _opts, cmd) => {
8749
9083
  const cfg = resolveConfig(cmd.optsWithGlobals());
8750
9084
  const data = await runApi("Accepting filing suggestion", async () => {
8751
9085
  const client = await makeClient(cfg);
8752
- return client.POST("/filing/suggestions/{id}/accept", { params: { path: { id } }, body: {} });
9086
+ return client.POST("/filing/suggestions/{id}/accept", {
9087
+ params: { path: { id } },
9088
+ body: {}
9089
+ });
8753
9090
  });
8754
- emitAction(`accepted filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9091
+ emitAction(
9092
+ `accepted filing suggestion ${style.bold(id)}`,
9093
+ data,
9094
+ cmd.optsWithGlobals().json
9095
+ );
8755
9096
  });
8756
- filing.command("reject <id>").description("Reject a filing suggestion (POST /filing/suggestions/{id}/reject)").option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
9097
+ filing.command("reject <id>").description(
9098
+ "Reject a filing suggestion (POST /filing/suggestions/{id}/reject)"
9099
+ ).option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
8757
9100
  const cfg = resolveConfig(cmd.optsWithGlobals());
8758
9101
  const data = await runApi("Rejecting filing suggestion", async () => {
8759
9102
  const client = await makeClient(cfg);
@@ -8765,9 +9108,18 @@ Examples:
8765
9108
  }
8766
9109
  });
8767
9110
  });
8768
- emitAction(`rejected filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9111
+ emitAction(
9112
+ `rejected filing suggestion ${style.bold(id)}`,
9113
+ data,
9114
+ cmd.optsWithGlobals().json
9115
+ );
8769
9116
  });
8770
- filing.command("defer <id>").description("Defer a filing suggestion (POST /filing/suggestions/{id}/defer)").option("--until <iso>", "Defer until an ISO-8601 timestamp (defaults to indefinite)").action(async (id, opts, cmd) => {
9117
+ filing.command("defer <id>").description(
9118
+ "Defer a filing suggestion (POST /filing/suggestions/{id}/defer)"
9119
+ ).option(
9120
+ "--until <iso>",
9121
+ "Defer until an ISO-8601 timestamp (defaults to indefinite)"
9122
+ ).action(async (id, opts, cmd) => {
8771
9123
  const cfg = resolveConfig(cmd.optsWithGlobals());
8772
9124
  const data = await runApi("Deferring filing suggestion", async () => {
8773
9125
  const client = await makeClient(cfg);
@@ -8776,25 +9128,44 @@ Examples:
8776
9128
  body: { until: opts.until ?? null }
8777
9129
  });
8778
9130
  });
8779
- emitAction(`deferred filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9131
+ emitAction(
9132
+ `deferred filing suggestion ${style.bold(id)}`,
9133
+ data,
9134
+ cmd.optsWithGlobals().json
9135
+ );
8780
9136
  });
8781
- filing.command("edit-and-accept <id>").description("Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)").option("--target-kind <kind>", "Workspace | Project").option("--existing-target-id <id>", "File into an existing workspace/project").option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option("--new-parent-workspace <workspaceId>", "Parent workspace for a new project").option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
9137
+ filing.command("edit-and-accept <id>").description(
9138
+ "Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)"
9139
+ ).option("--target-kind <kind>", "Workspace | Project").option(
9140
+ "--existing-target-id <id>",
9141
+ "File into an existing workspace/project"
9142
+ ).option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option(
9143
+ "--new-parent-workspace <workspaceId>",
9144
+ "Parent workspace for a new project"
9145
+ ).option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
8782
9146
  const cfg = resolveConfig(cmd.optsWithGlobals());
8783
- const data = await runApi("Editing and accepting filing suggestion", async () => {
8784
- const client = await makeClient(cfg);
8785
- return client.POST("/filing/suggestions/{id}/edit-and-accept", {
8786
- params: { path: { id } },
8787
- body: {
8788
- targetKind: opts.targetKind ?? null,
8789
- existingTargetId: opts.existingTargetId ?? null,
8790
- newName: opts.newName ?? null,
8791
- newDescription: opts.newDescription ?? null,
8792
- newParentWorkspaceId: opts.newParentWorkspace ?? null,
8793
- overrideMemoryIds: opts.memoryId ?? null
8794
- }
8795
- });
8796
- });
8797
- emitAction(`edited & accepted filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9147
+ const data = await runApi(
9148
+ "Editing and accepting filing suggestion",
9149
+ async () => {
9150
+ const client = await makeClient(cfg);
9151
+ return client.POST("/filing/suggestions/{id}/edit-and-accept", {
9152
+ params: { path: { id } },
9153
+ body: {
9154
+ targetKind: opts.targetKind ?? null,
9155
+ existingTargetId: opts.existingTargetId ?? null,
9156
+ newName: opts.newName ?? null,
9157
+ newDescription: opts.newDescription ?? null,
9158
+ newParentWorkspaceId: opts.newParentWorkspace ?? null,
9159
+ overrideMemoryIds: opts.memoryId ?? null
9160
+ }
9161
+ });
9162
+ }
9163
+ );
9164
+ emitAction(
9165
+ `edited & accepted filing suggestion ${style.bold(id)}`,
9166
+ data,
9167
+ cmd.optsWithGlobals().json
9168
+ );
8798
9169
  });
8799
9170
  }
8800
9171
 
@@ -10117,6 +10488,18 @@ function resolveCreateBody(textOpt, fileOpt) {
10117
10488
  );
10118
10489
  return { text: text2, defaultTitle };
10119
10490
  }
10491
+ async function fetchAllMemoryRelationships(cfg, memoryId) {
10492
+ return await fetchAllPages(
10493
+ (page) => runApi(`Fetching relationships (page ${page})`, async () => {
10494
+ const client = await makeClient(cfg);
10495
+ return client.GET("/memories/{memoryId}/relationships", {
10496
+ params: { path: { memoryId }, query: { page } }
10497
+ });
10498
+ }),
10499
+ {},
10500
+ "relationships"
10501
+ );
10502
+ }
10120
10503
  function registerMemory(program2) {
10121
10504
  const memory = program2.command("memory").description("Create, read, and search memories");
10122
10505
  memory.addHelpText(
@@ -10371,7 +10754,16 @@ ${plan.rows.map(
10371
10754
  false
10372
10755
  );
10373
10756
  });
10374
- memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
10757
+ const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
10758
+ "--with-relationships",
10759
+ "Include the memory's relationships in the response",
10760
+ false
10761
+ ).addHelpText(
10762
+ "after",
10763
+ `
10764
+ Note: CLI memory get omits relationships by default; MCP get_memory includes them. Use --with-relationships to read all relationship pages, or relationship list <memoryId> to inspect edges.`
10765
+ );
10766
+ get.action(async (memoryId, opts, cmd) => {
10375
10767
  const cfg = resolveConfig(cmd.optsWithGlobals());
10376
10768
  const data = await runApi("Fetching memory", async () => {
10377
10769
  const client = await makeClient(cfg);
@@ -10379,6 +10771,11 @@ ${plan.rows.map(
10379
10771
  params: { path: { memoryId } }
10380
10772
  });
10381
10773
  });
10774
+ if (opts.withRelationships) {
10775
+ const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
10776
+ emit({ ...data, relationships }, cmd.optsWithGlobals().json);
10777
+ return;
10778
+ }
10382
10779
  emit(data, cmd.optsWithGlobals().json);
10383
10780
  });
10384
10781
  memory.command("search <query>").description(
@@ -10601,21 +10998,28 @@ ${plan.rows.map(
10601
10998
  cmd.optsWithGlobals().json
10602
10999
  );
10603
11000
  });
10604
- memory.command("list-archived").description("List archived memories (GET /memories/archived)").option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
11001
+ memory.command("list-archived").description(
11002
+ "List archived memories (GET /memories/archived). Walks every page by default."
11003
+ ).option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
10605
11004
  const cfg = resolveConfig(cmd.optsWithGlobals());
10606
- const data = await runApi("Listing archived memories", async () => {
11005
+ const filters = {
11006
+ ...opts.workspace ? { workspaceId: opts.workspace } : {},
11007
+ ...opts.project ? { projectId: opts.project } : {}
11008
+ };
11009
+ const readPage = (query) => runApi("Listing archived memories", async () => {
10607
11010
  const client = await makeClient(cfg);
10608
11011
  return client.GET("/memories/archived", {
10609
- params: {
10610
- query: {
10611
- ...opts.workspace ? { workspaceId: opts.workspace } : {},
10612
- ...opts.project ? { projectId: opts.project } : {},
10613
- ...opts.page ? { page: Number(opts.page) } : {},
10614
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
10615
- }
10616
- }
11012
+ params: { query: { ...filters, ...query } }
10617
11013
  });
10618
11014
  });
11015
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
11016
+ (page) => readPage({
11017
+ page,
11018
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
11019
+ }),
11020
+ opts,
11021
+ "archived memories"
11022
+ ) : await readPage(singlePageQuery(opts));
10619
11023
  emit(data, cmd.optsWithGlobals().json);
10620
11024
  });
10621
11025
  memory.command("versions <memoryId>").description("List a memory's versions (GET /memories/{memoryId}/versions)").action(async (memoryId, _opts, cmd) => {
@@ -10726,8 +11130,18 @@ ${plan.rows.map(
10726
11130
  }
10727
11131
 
10728
11132
  // src/setup/apply.ts
10729
- import { createHash as createHash5 } from "crypto";
10730
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync15, existsSync as existsSync13 } from "fs";
11133
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
11134
+ import {
11135
+ chmodSync as chmodSync2,
11136
+ copyFileSync,
11137
+ existsSync as existsSync13,
11138
+ mkdirSync as mkdirSync16,
11139
+ readFileSync as readFileSync16,
11140
+ renameSync as renameSync4,
11141
+ rmSync as rmSync7,
11142
+ statSync as statSync5,
11143
+ writeFileSync as writeFileSync15
11144
+ } from "fs";
10731
11145
  import { dirname as dirname15 } from "path";
10732
11146
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
10733
11147
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -10773,11 +11187,17 @@ function parseManagedBlock(content, block) {
10773
11187
  const keyed = content.match(keyedBlockRe(block));
10774
11188
  if (keyed) {
10775
11189
  const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
10776
- return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
11190
+ return {
11191
+ block,
11192
+ source: attrs.source ?? null,
11193
+ sha256: attrs.sha256 ?? null,
11194
+ body: innerBody(keyed[0])
11195
+ };
10777
11196
  }
10778
11197
  if (block === "role-template") {
10779
11198
  const legacy = content.match(legacyBlockRe());
10780
- if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
11199
+ if (legacy)
11200
+ return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
10781
11201
  }
10782
11202
  return null;
10783
11203
  }
@@ -10799,56 +11219,469 @@ function mergeMcpJson(path, snippet, dryRun) {
10799
11219
  try {
10800
11220
  current = JSON.parse(readFileSync16(path, "utf8"));
10801
11221
  } catch {
10802
- return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
11222
+ return {
11223
+ kind: "mcp",
11224
+ path,
11225
+ status: "skipped",
11226
+ note: "existing file isn't valid JSON \u2014 left untouched"
11227
+ };
10803
11228
  }
10804
11229
  }
10805
- current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
11230
+ current.mcpServers = {
11231
+ ...current.mcpServers ?? {},
11232
+ ...incoming.mcpServers ?? {}
11233
+ };
10806
11234
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10807
11235
  ensureDir2(path);
10808
11236
  writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
10809
11237
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10810
11238
  }
10811
- function mergeCodexToml(path, snippet, dryRun) {
10812
- const existed = existsSync13(path);
10813
- let body = readOr(path, "");
10814
- body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
10815
- const trimmed = body.trim();
10816
- const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
10817
- if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10818
- ensureDir2(path);
10819
- writeFileSync15(path, next, { mode: 384 });
10820
- return { kind: "mcp", path, status: existed ? "merged" : "created" };
10821
- }
10822
- function writeInstructionBlock(path, write, dryRun) {
10823
- const existed = existsSync13(path);
10824
- const next = computeBlockFile(readOr(path, ""), write);
10825
- if (dryRun) return { kind: "instruction", path, status: "dry-run", block: write.block };
10826
- ensureDir2(path);
10827
- writeFileSync15(path, next);
10828
- return { kind: "instruction", path, status: existed ? "merged" : "created", block: write.block };
11239
+ var CODEX_MCP_TABLE = "mcp_servers.sechroom";
11240
+ function decodeTomlBasicString(value) {
11241
+ let decoded = "";
11242
+ for (let i = 0; i < value.length; i += 1) {
11243
+ const char = value[i];
11244
+ if (char !== "\\") {
11245
+ decoded += char;
11246
+ continue;
11247
+ }
11248
+ const escaped = value[++i];
11249
+ if (escaped === void 0) throw new Error("unterminated TOML escape");
11250
+ const simple = {
11251
+ b: "\b",
11252
+ f: "\f",
11253
+ n: "\n",
11254
+ r: "\r",
11255
+ t: " ",
11256
+ '"': '"',
11257
+ "\\": "\\"
11258
+ };
11259
+ if (escaped in simple) {
11260
+ decoded += simple[escaped];
11261
+ continue;
11262
+ }
11263
+ const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
11264
+ if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
11265
+ const code = value.slice(i + 1, i + 1 + digits);
11266
+ if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
11267
+ throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
11268
+ decoded += String.fromCodePoint(Number.parseInt(code, 16));
11269
+ i += digits;
11270
+ }
11271
+ return decoded;
10829
11272
  }
10830
- function computeBlockFile(current, write) {
10831
- const rendered = renderBlock(write);
10832
- const keyed = keyedBlockRe(write.block);
10833
- if (keyed.test(current)) return current.replace(keyed, rendered);
10834
- if (write.block === "role-template" && legacyBlockRe().test(current)) {
10835
- return current.replace(legacyBlockRe(), rendered);
11273
+ function normalizeTomlKey(raw) {
11274
+ const segments = [];
11275
+ let i = 0;
11276
+ while (i < raw.length) {
11277
+ while (/\s/.test(raw[i] ?? "")) i += 1;
11278
+ if (i >= raw.length) break;
11279
+ const quote = raw[i];
11280
+ let segment;
11281
+ if (quote === '"' || quote === "'") {
11282
+ i += 1;
11283
+ const start = i;
11284
+ let escaped = false;
11285
+ while (i < raw.length) {
11286
+ const char = raw[i];
11287
+ if (quote === '"' && escaped) {
11288
+ escaped = false;
11289
+ i += 1;
11290
+ continue;
11291
+ }
11292
+ if (quote === '"' && char === "\\") {
11293
+ escaped = true;
11294
+ i += 1;
11295
+ continue;
11296
+ }
11297
+ if (char === quote) break;
11298
+ i += 1;
11299
+ }
11300
+ if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
11301
+ const encoded = raw.slice(start, i);
11302
+ segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
11303
+ i += 1;
11304
+ } else {
11305
+ const start = i;
11306
+ while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
11307
+ segment = raw.slice(start, i);
11308
+ if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
11309
+ }
11310
+ segments.push(segment);
11311
+ while (/\s/.test(raw[i] ?? "")) i += 1;
11312
+ if (i >= raw.length) break;
11313
+ if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
11314
+ i += 1;
11315
+ }
11316
+ if (segments.length === 0) throw new Error("empty TOML key");
11317
+ return segments;
11318
+ }
11319
+ function sameTomlKey(left, right) {
11320
+ return left.length === right.length && left.every((segment, index) => segment === right[index]);
11321
+ }
11322
+ function isTomlKeyPrefix(prefix, candidate) {
11323
+ return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
11324
+ }
11325
+ function tomlKeyLabel(key) {
11326
+ return key.join(".");
11327
+ }
11328
+ function stripTomlComment(line) {
11329
+ let quote = null;
11330
+ let escaped = false;
11331
+ for (let i = 0; i < line.length; i += 1) {
11332
+ const rest = line.slice(i);
11333
+ const char = line[i];
11334
+ if (quote === "multiline-basic") {
11335
+ if (rest.startsWith('"""') && !escaped) {
11336
+ escaped = false;
11337
+ quote = null;
11338
+ i += 2;
11339
+ continue;
11340
+ }
11341
+ if (escaped) escaped = false;
11342
+ else if (char === "\\") escaped = true;
11343
+ continue;
11344
+ }
11345
+ if (quote === "multiline-literal") {
11346
+ if (rest.startsWith("'''")) {
11347
+ i += 2;
11348
+ quote = null;
11349
+ }
11350
+ continue;
11351
+ }
11352
+ if (quote === "basic") {
11353
+ if (escaped) escaped = false;
11354
+ else if (char === "\\") escaped = true;
11355
+ else if (char === '"') quote = null;
11356
+ continue;
11357
+ }
11358
+ if (quote === "literal") {
11359
+ if (char === "'") quote = null;
11360
+ continue;
11361
+ }
11362
+ if (rest.startsWith('"""')) {
11363
+ quote = "multiline-basic";
11364
+ i += 2;
11365
+ } else if (rest.startsWith("'''")) {
11366
+ quote = "multiline-literal";
11367
+ i += 2;
11368
+ } else if (char === '"') quote = "basic";
11369
+ else if (char === "'") quote = "literal";
11370
+ else if (char === "#") return line.slice(0, i);
11371
+ }
11372
+ return line;
11373
+ }
11374
+ function scanTomlValue(value, state) {
11375
+ for (let i = 0; i < value.length; i += 1) {
11376
+ const rest = value.slice(i);
11377
+ const char = value[i];
11378
+ if (state.quote === "multiline-basic") {
11379
+ if (rest.startsWith('"""') && !state.escaped) {
11380
+ state.quote = null;
11381
+ i += 2;
11382
+ continue;
11383
+ }
11384
+ if (state.escaped) state.escaped = false;
11385
+ else if (char === "\\") state.escaped = true;
11386
+ continue;
11387
+ }
11388
+ if (state.quote === "multiline-literal") {
11389
+ if (rest.startsWith("'''")) {
11390
+ state.quote = null;
11391
+ i += 2;
11392
+ }
11393
+ continue;
11394
+ }
11395
+ if (state.quote === "basic") {
11396
+ if (state.escaped) state.escaped = false;
11397
+ else if (char === "\\") state.escaped = true;
11398
+ else if (char === '"') state.quote = null;
11399
+ continue;
11400
+ }
11401
+ if (state.quote === "literal") {
11402
+ if (char === "'") state.quote = null;
11403
+ continue;
11404
+ }
11405
+ if (char === "#") break;
11406
+ if (rest.startsWith('"""')) {
11407
+ state.quote = "multiline-basic";
11408
+ i += 2;
11409
+ } else if (rest.startsWith("'''")) {
11410
+ state.quote = "multiline-literal";
11411
+ i += 2;
11412
+ } else if (char === '"') state.quote = "basic";
11413
+ else if (char === "'") state.quote = "literal";
11414
+ else if (char === "[" || char === "{") state.containers.push(char);
11415
+ else if (char === "]" || char === "}") {
11416
+ const expected = char === "]" ? "[" : "{";
11417
+ if (state.containers.pop() !== expected)
11418
+ throw new Error(`unbalanced TOML delimiter '${char}'`);
11419
+ }
11420
+ }
11421
+ }
11422
+ function parseTomlHeader(rawLine) {
11423
+ const line = stripTomlComment(rawLine).trim();
11424
+ const array = line.startsWith("[[") && line.endsWith("]]");
11425
+ const table = !array && line.startsWith("[") && line.endsWith("]");
11426
+ if (!array && !table) return null;
11427
+ const prefix = array ? 2 : 1;
11428
+ const suffix = array ? 2 : 1;
11429
+ const rawName = line.slice(prefix, line.length - suffix).trim();
11430
+ if (!rawName) throw new Error("empty TOML table name");
11431
+ return { name: normalizeTomlKey(rawName), array };
11432
+ }
11433
+ function findTomlEquals(line) {
11434
+ let quote = null;
11435
+ let escaped = false;
11436
+ for (let i = 0; i < line.length; i += 1) {
11437
+ const char = line[i];
11438
+ if (quote === "basic") {
11439
+ if (escaped) escaped = false;
11440
+ else if (char === "\\") escaped = true;
11441
+ else if (char === '"') quote = null;
11442
+ continue;
11443
+ }
11444
+ if (quote === "literal") {
11445
+ if (char === "'") quote = null;
11446
+ continue;
11447
+ }
11448
+ if (char === '"') quote = "basic";
11449
+ else if (char === "'") quote = "literal";
11450
+ else if (char === "=") return i;
10836
11451
  }
10837
- return current.trim().length > 0 ? `${current.trimEnd()}
10838
-
10839
- ${rendered}` : rendered;
11452
+ return -1;
10840
11453
  }
10841
- function evaluateBlock(content, block, serverBody) {
10842
- const onDisk = parseManagedBlock(content, block);
10843
- if (!onDisk) {
10844
- const hasManagedMarker = content.includes(MARKER_BEGIN) || content.includes(MARKER_END);
10845
- if (!hasManagedMarker && content.includes(BOOTSTRAP_STUB_REFUSAL))
10846
- return "stub";
10847
- return "absent";
11454
+ function arrayTableContext(name, arrays) {
11455
+ let best = null;
11456
+ for (let i = arrays.length - 1; i >= 0; i -= 1) {
11457
+ const candidate = arrays[i];
11458
+ if (isTomlKeyPrefix(candidate.key, name)) {
11459
+ if (!best || candidate.key.length > best.key.length) best = candidate;
11460
+ }
10848
11461
  }
10849
- const actual = bodySha256(onDisk.body);
10850
- if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
10851
- return actual === bodySha256(serverBody) ? "current" : "stale";
11462
+ return best;
11463
+ }
11464
+ function tomlTableIdentity(key, context) {
11465
+ return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
11466
+ }
11467
+ function isTomlScalar(value) {
11468
+ const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
11469
+ const number = new RegExp(
11470
+ `^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
11471
+ );
11472
+ const datetime = /^(?:[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?|[0-9]{4}-[0-9]{2}-[0-9]{2}(?:(?:[Tt ]|[Tt])[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})?)?)$/;
11473
+ return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
11474
+ }
11475
+ function validateTomlValueShape(value) {
11476
+ const token = stripTomlComment(value).trim();
11477
+ if (!token) throw new Error("empty TOML value");
11478
+ if (token.startsWith('"') || token.startsWith("'")) {
11479
+ const quote = token[0];
11480
+ if (quote && token.endsWith(quote)) return;
11481
+ }
11482
+ if (token.startsWith("[") && token.endsWith("]")) return;
11483
+ if (token.startsWith("{") && token.endsWith("}")) return;
11484
+ if (isTomlScalar(token)) return;
11485
+ throw new Error(`invalid TOML value '${token}'`);
11486
+ }
11487
+ function validateToml(content) {
11488
+ const tables = /* @__PURE__ */ new Set();
11489
+ const keys = /* @__PURE__ */ new Set();
11490
+ const arrayInstances = [];
11491
+ let currentTable = { identity: "root" };
11492
+ let valueState = {
11493
+ containers: [],
11494
+ quote: null,
11495
+ escaped: false
11496
+ };
11497
+ for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
11498
+ if (valueState.quote || valueState.containers.length > 0) {
11499
+ scanTomlValue(rawLine, valueState);
11500
+ continue;
11501
+ }
11502
+ const line = stripTomlComment(rawLine).trim();
11503
+ if (!line) continue;
11504
+ const header = parseTomlHeader(rawLine);
11505
+ if (header) {
11506
+ if (header.array) {
11507
+ const context = arrayTableContext(header.name, arrayInstances);
11508
+ const parentIdentity = context?.identity ?? null;
11509
+ const previous = [...arrayInstances].reverse().find(
11510
+ (candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
11511
+ );
11512
+ const count = (previous?.count ?? 0) + 1;
11513
+ const instance = {
11514
+ key: header.name,
11515
+ parentIdentity,
11516
+ identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
11517
+ count
11518
+ };
11519
+ arrayInstances.push(instance);
11520
+ currentTable = { identity: instance.identity };
11521
+ } else {
11522
+ const context = arrayTableContext(header.name, arrayInstances);
11523
+ currentTable = {
11524
+ identity: tomlTableIdentity(header.name, context)
11525
+ };
11526
+ if (tables.has(currentTable.identity))
11527
+ throw new Error(
11528
+ `duplicate TOML table '${tomlKeyLabel(header.name)}'`
11529
+ );
11530
+ tables.add(currentTable.identity);
11531
+ }
11532
+ continue;
11533
+ }
11534
+ const equals = findTomlEquals(line);
11535
+ if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
11536
+ const key = normalizeTomlKey(line.slice(0, equals).trim());
11537
+ const value = line.slice(equals + 1).trim();
11538
+ if (!value || value.startsWith("#"))
11539
+ throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
11540
+ const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
11541
+ if (keys.has(fullKey))
11542
+ throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
11543
+ keys.add(fullKey);
11544
+ scanTomlValue(line.slice(equals + 1), valueState);
11545
+ if (valueState.quote === "basic" || valueState.quote === "literal")
11546
+ throw new Error("unterminated TOML string");
11547
+ if (!valueState.quote && valueState.containers.length === 0)
11548
+ validateTomlValueShape(value);
11549
+ }
11550
+ if (valueState.quote) throw new Error("unterminated TOML string");
11551
+ if (valueState.containers.length > 0)
11552
+ throw new Error("unbalanced TOML value");
11553
+ }
11554
+ function isTomlTableHeader(line) {
11555
+ try {
11556
+ return parseTomlHeader(line) !== null;
11557
+ } catch {
11558
+ return false;
11559
+ }
11560
+ }
11561
+ function replaceTomlTable(content, tableName, replacement) {
11562
+ const target = normalizeTomlKey(tableName);
11563
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
11564
+ const replacementLines = replacement.trim().split("\n");
11565
+ const kept = [];
11566
+ let i = 0;
11567
+ while (i < lines.length) {
11568
+ const header = parseTomlHeader(lines[i]);
11569
+ if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
11570
+ i += 1;
11571
+ while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
11572
+ continue;
11573
+ }
11574
+ kept.push(lines[i]);
11575
+ i += 1;
11576
+ }
11577
+ const base = kept.join("\n").trim();
11578
+ return `${base.length > 0 ? `${base}
11579
+
11580
+ ` : ""}${replacementLines.join("\n")}
11581
+ `;
11582
+ }
11583
+ var defaultTomlFileOperations = {
11584
+ copyFileSync,
11585
+ renameSync: renameSync4
11586
+ };
11587
+ function writeTomlAtomic(path, content, fileOperations = {}) {
11588
+ ensureDir2(path);
11589
+ const operations = { ...defaultTomlFileOperations, ...fileOperations };
11590
+ const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11591
+ const backup = `${path}.bak`;
11592
+ const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11593
+ const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
11594
+ try {
11595
+ writeFileSync15(temporary, content, { mode });
11596
+ chmodSync2(temporary, mode);
11597
+ validateToml(readFileSync16(temporary, "utf8"));
11598
+ if (existsSync13(path) && !existsSync13(backup)) {
11599
+ operations.copyFileSync(path, backupTemporary);
11600
+ chmodSync2(backupTemporary, mode);
11601
+ validateToml(readFileSync16(backupTemporary, "utf8"));
11602
+ operations.renameSync(backupTemporary, backup);
11603
+ }
11604
+ operations.renameSync(temporary, path);
11605
+ } finally {
11606
+ rmSync7(temporary, { force: true });
11607
+ rmSync7(backupTemporary, { force: true });
11608
+ }
11609
+ }
11610
+ function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
11611
+ const existed = existsSync13(path);
11612
+ const body = readOr(path, "");
11613
+ try {
11614
+ validateToml(body);
11615
+ } catch (error) {
11616
+ return {
11617
+ kind: "mcp",
11618
+ path,
11619
+ status: "skipped",
11620
+ note: `existing TOML is invalid \u2014 left untouched (${error.message})`
11621
+ };
11622
+ }
11623
+ const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
11624
+ try {
11625
+ validateToml(next);
11626
+ } catch (error) {
11627
+ return {
11628
+ kind: "mcp",
11629
+ path,
11630
+ status: "skipped",
11631
+ note: `generated TOML is invalid \u2014 left untouched (${error.message})`
11632
+ };
11633
+ }
11634
+ if (dryRun) return { kind: "mcp", path, status: "dry-run" };
11635
+ if (next === body)
11636
+ return { kind: "mcp", path, status: existed ? "current" : "created" };
11637
+ try {
11638
+ writeTomlAtomic(path, next, fileOperations);
11639
+ } catch (error) {
11640
+ return {
11641
+ kind: "mcp",
11642
+ path,
11643
+ status: "skipped",
11644
+ note: `could not replace TOML \u2014 left untouched (${error.message})`
11645
+ };
11646
+ }
11647
+ return { kind: "mcp", path, status: existed ? "merged" : "created" };
11648
+ }
11649
+ function writeInstructionBlock(path, write, dryRun) {
11650
+ const existed = existsSync13(path);
11651
+ const next = computeBlockFile(readOr(path, ""), write);
11652
+ if (dryRun)
11653
+ return { kind: "instruction", path, status: "dry-run", block: write.block };
11654
+ ensureDir2(path);
11655
+ writeFileSync15(path, next);
11656
+ return {
11657
+ kind: "instruction",
11658
+ path,
11659
+ status: existed ? "merged" : "created",
11660
+ block: write.block
11661
+ };
11662
+ }
11663
+ function computeBlockFile(current, write) {
11664
+ const rendered = renderBlock(write);
11665
+ const keyed = keyedBlockRe(write.block);
11666
+ if (keyed.test(current)) return current.replace(keyed, rendered);
11667
+ if (write.block === "role-template" && legacyBlockRe().test(current)) {
11668
+ return current.replace(legacyBlockRe(), rendered);
11669
+ }
11670
+ return current.trim().length > 0 ? `${current.trimEnd()}
11671
+
11672
+ ${rendered}` : rendered;
11673
+ }
11674
+ function evaluateBlock(content, block, serverBody) {
11675
+ const onDisk = parseManagedBlock(content, block);
11676
+ if (!onDisk) {
11677
+ const hasManagedMarker = content.includes(MARKER_BEGIN) || content.includes(MARKER_END);
11678
+ if (!hasManagedMarker && content.includes(BOOTSTRAP_STUB_REFUSAL))
11679
+ return "stub";
11680
+ return "absent";
11681
+ }
11682
+ const actual = bodySha256(onDisk.body);
11683
+ if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
11684
+ return actual === bodySha256(serverBody) ? "current" : "stale";
10852
11685
  }
10853
11686
  function applyBlock(path, write, mode, dryRun) {
10854
11687
  const current = readOr(path, "");
@@ -10874,7 +11707,13 @@ function applyBlock(path, write, mode, dryRun) {
10874
11707
  };
10875
11708
  }
10876
11709
  if (state === "current") {
10877
- return { kind: "instruction", path, status: "current", eval: "current", block: write.block };
11710
+ return {
11711
+ kind: "instruction",
11712
+ path,
11713
+ status: "current",
11714
+ eval: "current",
11715
+ block: write.block
11716
+ };
10878
11717
  }
10879
11718
  if (state === "drift" && mode !== "force") {
10880
11719
  const proposedPath = `${path}.proposed`;
@@ -10909,7 +11748,12 @@ async function applyClient(cfg, setup, target, opts) {
10909
11748
  const section = findSection(surface, target.mcp.sectionType);
10910
11749
  const snippet = sectionSnippet(section);
10911
11750
  if (!snippet) {
10912
- actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
11751
+ actions.push({
11752
+ kind: "mcp",
11753
+ path: target.mcp.path,
11754
+ status: "skipped",
11755
+ note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
11756
+ });
10913
11757
  } else {
10914
11758
  actions.push(
10915
11759
  target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
@@ -10920,25 +11764,52 @@ async function applyClient(cfg, setup, target, opts) {
10920
11764
  const surface = findSurface(setup, target.instruction.surfaceKey);
10921
11765
  const section = findSection(surface, SectionType.InstructionFile);
10922
11766
  if (!section) {
10923
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
11767
+ actions.push({
11768
+ kind: "instruction",
11769
+ path: target.instruction.path,
11770
+ status: "skipped",
11771
+ note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
11772
+ });
10924
11773
  } else {
10925
11774
  const resolved = await withSpinner(
10926
11775
  `Resolving ${target.label} agent instructions`,
10927
11776
  () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
10928
11777
  );
10929
11778
  if (!resolved) {
10930
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
11779
+ actions.push({
11780
+ kind: "instruction",
11781
+ path: target.instruction.path,
11782
+ status: "skipped",
11783
+ // Name the block even when nothing resolved. Without it the skip reports a
11784
+ // path and no identity, so a consumer cannot tell WHICH managed block went
11785
+ // missing — absent rather than loud, which is the failure this guards.
11786
+ block: "role-template",
11787
+ // GUARD — an unresolved template must NAME what it looked for. A bare
11788
+ // "not found" reads identically whether the bundle is absent or the memo
11789
+ // was retagged out of every candidate family, and the second case is a
11790
+ // silent loss of a template the installation previously had.
11791
+ 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`"
11792
+ });
10931
11793
  } else {
10932
11794
  const action = applyBlock(
10933
11795
  target.instruction.path,
10934
- { block: "role-template", body: resolved.body, source: resolved.sourceRef },
11796
+ {
11797
+ block: "role-template",
11798
+ body: resolved.body,
11799
+ source: resolved.sourceRef
11800
+ },
10935
11801
  mode,
10936
11802
  opts.dryRun
10937
11803
  );
10938
- actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
11804
+ actions.push(
11805
+ resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
11806
+ );
10939
11807
  }
10940
11808
  }
10941
- const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
11809
+ const conventionsSection = findSection(
11810
+ surface,
11811
+ SectionType.WorkspaceConventions
11812
+ );
10942
11813
  if (conventionsSection) {
10943
11814
  const conventions = await withSpinner(
10944
11815
  `Composing ${target.label} workspace conventions`,
@@ -10947,11 +11818,20 @@ async function applyClient(cfg, setup, target, opts) {
10947
11818
  if (conventions) {
10948
11819
  const action = applyBlock(
10949
11820
  target.instruction.path,
10950
- { block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
11821
+ {
11822
+ block: "workspace-conventions",
11823
+ body: conventions.body,
11824
+ source: `workspace:${cfg.workspaceId ?? ""}`
11825
+ },
10951
11826
  mode,
10952
11827
  opts.dryRun
10953
11828
  );
10954
- actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
11829
+ actions.push(
11830
+ action.status === "current" ? action : {
11831
+ ...action,
11832
+ note: action.note ?? `workspace conventions (${conventions.refs.length})`
11833
+ }
11834
+ );
10955
11835
  }
10956
11836
  }
10957
11837
  }
@@ -11169,7 +12049,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
11169
12049
 
11170
12050
  ${body}
11171
12051
  `,
11172
- tags: ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"]
12052
+ tags: [
12053
+ "agent-setup-bundle",
12054
+ "scope:sechroom",
12055
+ `kind:${kind}`,
12056
+ "archetype:document"
12057
+ ]
11173
12058
  };
11174
12059
  }
11175
12060
  function copyChoice(opts) {
@@ -11182,9 +12067,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
11182
12067
  const instr = targets[key]?.instruction;
11183
12068
  if (!instr || seen.has(instr.surfaceKey)) continue;
11184
12069
  seen.add(instr.surfaceKey);
11185
- const section = findSection(findSurface(setup, instr.surfaceKey), SectionType.InstructionFile);
12070
+ const section = findSection(
12071
+ findSurface(setup, instr.surfaceKey),
12072
+ SectionType.InstructionFile
12073
+ );
11186
12074
  if (!section) continue;
11187
- const resolved = await resolveInstruction(cfg, section, personalWorkspaceId);
12075
+ const resolved = await resolveInstruction(
12076
+ cfg,
12077
+ section,
12078
+ personalWorkspaceId
12079
+ );
11188
12080
  if (!resolved || resolved.source === "override") continue;
11189
12081
  let make = choice === "yes";
11190
12082
  if (choice === "ask") {
@@ -11199,8 +12091,10 @@ version, the shared template stays clean, and you can discard back anytime.
11199
12091
  }
11200
12092
  if (make) {
11201
12093
  await createOverride(cfg, resolved, personalWorkspaceId);
11202
- process.stderr.write(`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
11203
- `);
12094
+ process.stderr.write(
12095
+ `\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
12096
+ `
12097
+ );
11204
12098
  }
11205
12099
  }
11206
12100
  }
@@ -11210,7 +12104,9 @@ function resolveClientKeys(raw) {
11210
12104
  if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
11211
12105
  for (const k of tokens) {
11212
12106
  if (!targets[k]) {
11213
- fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
12107
+ fail(
12108
+ `unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
12109
+ );
11214
12110
  }
11215
12111
  }
11216
12112
  return [...new Set(tokens)];
@@ -11221,8 +12117,10 @@ ${client.label} (${client.key}):
11221
12117
  `);
11222
12118
  for (const a of actions) {
11223
12119
  const tag = a.status === "skipped" ? "skip" : a.status;
11224
- process.stdout.write(` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11225
- `);
12120
+ process.stdout.write(
12121
+ ` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
12122
+ `
12123
+ );
11226
12124
  }
11227
12125
  }
11228
12126
  function resolveEvalMode(opts) {
@@ -11237,8 +12135,17 @@ function buildCheckReport(result) {
11237
12135
  stub: 0
11238
12136
  };
11239
12137
  const blocks = [];
12138
+ const unresolved = [];
11240
12139
  for (const { client, actions } of result) {
11241
12140
  for (const action of actions) {
12141
+ if (action.kind === "instruction" && action.status === "skipped" && !action.eval) {
12142
+ unresolved.push({
12143
+ client,
12144
+ path: action.path,
12145
+ block: action.block ?? "unknown",
12146
+ note: action.note
12147
+ });
12148
+ }
11242
12149
  if (!action.eval) continue;
11243
12150
  counts[action.eval]++;
11244
12151
  blocks.push({
@@ -11252,7 +12159,8 @@ function buildCheckReport(result) {
11252
12159
  return {
11253
12160
  eval: counts,
11254
12161
  wouldChange: counts.stale + counts.drift + counts.absent,
11255
- blocks
12162
+ blocks,
12163
+ unresolved
11256
12164
  };
11257
12165
  }
11258
12166
  function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
@@ -11267,7 +12175,7 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
11267
12175
  },
11268
12176
  true
11269
12177
  );
11270
- } else if (report.wouldChange === 0) {
12178
+ } else if (report.wouldChange === 0 && report.unresolved.length === 0) {
11271
12179
  if (report.eval.stub) {
11272
12180
  const files = report.eval.stub === 1 ? "file" : "files";
11273
12181
  process.stdout.write(
@@ -11279,11 +12187,27 @@ function reportCheckAndExit(result, json, refreshCommand, jsonContext = {}) {
11279
12187
  }
11280
12188
  } else {
11281
12189
  const bits = [];
11282
- if (report.eval.stale) bits.push(`${report.eval.stale} out of date`);
11283
- if (report.eval.drift) bits.push(`${report.eval.drift} with local edits`);
11284
- if (report.eval.absent) bits.push(`${report.eval.absent} not yet written`);
12190
+ if (report.wouldChange > 0) {
12191
+ const changes = [];
12192
+ if (report.eval.stale) changes.push(`${report.eval.stale} out of date`);
12193
+ if (report.eval.drift)
12194
+ changes.push(`${report.eval.drift} with local edits`);
12195
+ if (report.eval.absent)
12196
+ changes.push(`${report.eval.absent} not yet written`);
12197
+ bits.push(
12198
+ `${report.wouldChange} instruction block(s) would change: ${changes.join(", ")}`
12199
+ );
12200
+ }
12201
+ if (report.unresolved.length > 0) {
12202
+ const details = report.unresolved.map(
12203
+ ({ client, path, block, note }) => `${client} ${block} at ${path}${note ? ` \u2014 ${note}` : ""}`
12204
+ ).join("; ");
12205
+ bits.push(
12206
+ `${report.unresolved.length} instruction template(s) skipped or unresolved: ${details}`
12207
+ );
12208
+ }
11285
12209
  process.stderr.write(
11286
- `\u26A0 ${report.wouldChange} instruction block(s) would change: ${bits.join(", ")}. Run ${style.cyan(refreshCommand)}.
12210
+ `\u26A0 ${bits.join("; ")}. Run ${style.cyan(refreshCommand)}.
11287
12211
  `
11288
12212
  );
11289
12213
  }
@@ -11296,8 +12220,10 @@ function summarizeEval(result, mode, json, dryRun) {
11296
12220
  const { eval: counts } = buildCheckReport(result);
11297
12221
  if (json) return;
11298
12222
  if (!dryRun && counts.stale) {
11299
- process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
11300
- `);
12223
+ process.stderr.write(
12224
+ `\u21BB refreshed ${counts.stale} section(s) the server had moved
12225
+ `
12226
+ );
11301
12227
  }
11302
12228
  if (!dryRun && counts.drift) {
11303
12229
  process.stderr.write(
@@ -11328,7 +12254,39 @@ async function resolveNamespaceChoice(cfg, flag) {
11328
12254
  return picked === GLOBAL_NAMESPACE ? null : picked;
11329
12255
  }
11330
12256
  function registerInit(program2) {
11331
- program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global", "global").option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option("--agent-files-only", "only write agent instruction files (skip MCP config)", false).option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").option("--refresh", "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite agent-file managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).addHelpText(
12257
+ program2.command("init").description(
12258
+ "Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
12259
+ ).option(
12260
+ "--client <list...>",
12261
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
12262
+ DEFAULT_CLIENT_KEY
12263
+ ).option(
12264
+ "--scope <scope>",
12265
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
12266
+ "global"
12267
+ ).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
12268
+ "--agent-files-only",
12269
+ "only write agent instruction files (skip MCP config)",
12270
+ false
12271
+ ).option(
12272
+ "--copy",
12273
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
12274
+ ).option(
12275
+ "--namespace <slug>",
12276
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
12277
+ ).option(
12278
+ "--refresh",
12279
+ "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
12280
+ false
12281
+ ).option(
12282
+ "--force",
12283
+ "rewrite agent-file managed blocks, overwriting local edits inside the markers",
12284
+ false
12285
+ ).option(
12286
+ "--check",
12287
+ "report whether agent files would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
12288
+ false
12289
+ ).addHelpText(
11332
12290
  "after",
11333
12291
  `
11334
12292
  Examples:
@@ -11343,7 +12301,9 @@ Examples:
11343
12301
  const mode = resolveEvalMode(opts);
11344
12302
  const check = mode === "check";
11345
12303
  if (check && opts.mcpOnly) {
11346
- fail("--check inspects agent files and cannot be combined with --mcp-only.");
12304
+ fail(
12305
+ "--check inspects agent files and cannot be combined with --mcp-only."
12306
+ );
11347
12307
  }
11348
12308
  const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
11349
12309
  const setup = await withSpinner(
@@ -11357,14 +12317,28 @@ Examples:
11357
12317
  } catch (err2) {
11358
12318
  return fail(err2.message);
11359
12319
  }
11360
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
12320
+ const claudeTargets = resolveClaudeTargets({
12321
+ override: g.claudeConfigDir,
12322
+ scope,
12323
+ cwd: process.cwd()
12324
+ });
11361
12325
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
11362
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
12326
+ const targets = clientTargets(process.cwd(), {
12327
+ claudeDir: claudeTargets[0]?.dir,
12328
+ codexHome: codexHomes[0] ?? null
12329
+ });
11363
12330
  const keys = resolveClientKeys(opts.client);
11364
12331
  const json = g.json;
11365
12332
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11366
12333
  if (!opts.dryRun && !opts.mcpOnly && !check) {
11367
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
12334
+ await maybeOfferCopies(
12335
+ cfg,
12336
+ setup,
12337
+ targets,
12338
+ keys,
12339
+ personalWorkspaceId,
12340
+ copyChoice(opts)
12341
+ );
11368
12342
  }
11369
12343
  const result = [];
11370
12344
  for (const key of keys) {
@@ -11382,18 +12356,33 @@ Examples:
11382
12356
  summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
11383
12357
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11384
12358
  for (const t of claudeTargets) {
11385
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code", configDir: t.dir });
12359
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
12360
+ yes: false,
12361
+ dryRun: Boolean(opts.dryRun),
12362
+ surface: "claude-code",
12363
+ configDir: t.dir
12364
+ });
11386
12365
  }
11387
12366
  }
11388
12367
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11389
- await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
12368
+ await maybeOfferHooks({
12369
+ yes: false,
12370
+ dryRun: Boolean(opts.dryRun),
12371
+ cwd: process.cwd(),
12372
+ scope,
12373
+ claudeConfigDir: g.claudeConfigDir,
12374
+ codexHome: g.codexHome
12375
+ });
11390
12376
  }
11391
12377
  if (json) {
11392
12378
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
11393
12379
  return;
11394
12380
  }
11395
12381
  const first = targets[keys[0]];
11396
- const surface = findSurface(setup, first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? "");
12382
+ const surface = findSurface(
12383
+ setup,
12384
+ first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
12385
+ );
11397
12386
  const verify = findSection(surface, SectionType.Verify);
11398
12387
  if (verify?.description) {
11399
12388
  process.stdout.write(`
@@ -11407,13 +12396,64 @@ Next \u2014 verify: ${verify.description}
11407
12396
  }
11408
12397
  function registerSetup(program2, deps = {}) {
11409
12398
  const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
11410
- setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
11411
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
11412
- });
11413
- setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").option("--refresh", "refresh out-of-date blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block").action(async (clients, opts, cmd) => {
11414
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy, mode: resolveEvalMode(opts) });
12399
+ setup.command("mcp <clients...>").description(
12400
+ `Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12401
+ ).option("--dry-run", "print what would be written without writing", false).option(
12402
+ "--namespace <slug>",
12403
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
12404
+ ).addHelpText(
12405
+ "after",
12406
+ "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
12407
+ ).action(async (clients, opts, cmd) => {
12408
+ await runClients(clients, cmd, {
12409
+ dryRun: Boolean(opts.dryRun),
12410
+ mcp: true,
12411
+ agentFiles: false,
12412
+ namespace: opts.namespace
12413
+ });
12414
+ });
12415
+ setup.command("agent-files <clients...>").description(
12416
+ `Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12417
+ ).option("--dry-run", "print what would be written without writing", false).option(
12418
+ "--copy",
12419
+ "make a personal copy you can edit (default: prompt on a TTY, else skip)"
12420
+ ).option(
12421
+ "--refresh",
12422
+ "refresh out-of-date blocks in place (local edits preserved to .proposed)",
12423
+ false
12424
+ ).option(
12425
+ "--force",
12426
+ "rewrite managed blocks, overwriting local edits inside the markers",
12427
+ false
12428
+ ).option(
12429
+ "--check",
12430
+ "report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
12431
+ false
12432
+ ).addHelpText(
12433
+ "after",
12434
+ "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block"
12435
+ ).action(async (clients, opts, cmd) => {
12436
+ await runClients(clients, cmd, {
12437
+ dryRun: Boolean(opts.dryRun),
12438
+ mcp: false,
12439
+ agentFiles: true,
12440
+ copy: opts.copy,
12441
+ mode: resolveEvalMode(opts)
12442
+ });
11415
12443
  });
11416
- setup.command("new-convention <title...>").description("Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files").option("--kind <kind>", "reference | standard (orders the section; reference first)", "reference").option("--workspace <id>", "workspace to author in (default: the bound workspace)").option("--body <markdown>", "section body (default: a TODO scaffold to edit later)").option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
12444
+ setup.command("new-convention <title...>").description(
12445
+ "Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
12446
+ ).option(
12447
+ "--kind <kind>",
12448
+ "reference | standard (orders the section; reference first)",
12449
+ "reference"
12450
+ ).option(
12451
+ "--workspace <id>",
12452
+ "workspace to author in (default: the bound workspace)"
12453
+ ).option(
12454
+ "--body <markdown>",
12455
+ "section body (default: a TODO scaffold to edit later)"
12456
+ ).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
11417
12457
  "after",
11418
12458
  `
11419
12459
  The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
@@ -11429,10 +12469,15 @@ Examples:
11429
12469
  const cfg = resolveConfig(cmd.optsWithGlobals());
11430
12470
  const json = Boolean(cmd.optsWithGlobals().json);
11431
12471
  const title = titleParts.join(" ").trim();
11432
- if (!title) fail('a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.');
12472
+ if (!title)
12473
+ fail(
12474
+ 'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
12475
+ );
11433
12476
  const workspaceId = opts.workspace ?? cfg.workspaceId;
11434
12477
  if (!workspaceId)
11435
- fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
12478
+ fail(
12479
+ "no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
12480
+ );
11436
12481
  const draft = buildConventionDraft(title, opts.kind, opts.body);
11437
12482
  if (opts.dryRun) {
11438
12483
  emit(
@@ -11473,7 +12518,10 @@ Examples:
11473
12518
  }
11474
12519
  if (opts.regen === false) {
11475
12520
  if (json) emit({ id: data.id, workspaceId, regen: false }, true);
11476
- else process.stdout.write("Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n");
12521
+ else
12522
+ process.stdout.write(
12523
+ "Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
12524
+ );
11477
12525
  return;
11478
12526
  }
11479
12527
  const regenerate = deps.regenerateConvention ?? runClients;
@@ -11502,7 +12550,14 @@ async function runClients(clients, cmd, opts) {
11502
12550
  );
11503
12551
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11504
12552
  if (opts.agentFiles && !opts.dryRun && !check) {
11505
- await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
12553
+ await maybeOfferCopies(
12554
+ cfg,
12555
+ setupData,
12556
+ targets,
12557
+ keys,
12558
+ personalWorkspaceId,
12559
+ copyChoice(opts)
12560
+ );
11506
12561
  }
11507
12562
  const json = g.json;
11508
12563
  const result = [];
@@ -11523,7 +12578,9 @@ async function runClients(clients, cmd, opts) {
11523
12578
  emit({ dryRun: opts.dryRun, clients: result }, true);
11524
12579
  return;
11525
12580
  }
11526
- process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
12581
+ process.stdout.write(
12582
+ opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
12583
+ );
11527
12584
  }
11528
12585
 
11529
12586
  // src/commands/namespace.ts
@@ -11599,7 +12656,7 @@ import { basename as basename5, join as join21 } from "path";
11599
12656
 
11600
12657
  // src/commands/fanout.ts
11601
12658
  import { spawnSync } from "child_process";
11602
- import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
12659
+ import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
11603
12660
  import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
11604
12661
  var ICON = {
11605
12662
  refresh: "\u21BB",
@@ -11622,7 +12679,7 @@ function discoverChildren(root) {
11622
12679
  if (name.startsWith(".") || name === "node_modules") continue;
11623
12680
  const dir = join20(root, name);
11624
12681
  try {
11625
- if (!statSync5(dir).isDirectory()) continue;
12682
+ if (!statSync6(dir).isDirectory()) continue;
11626
12683
  } catch {
11627
12684
  continue;
11628
12685
  }
@@ -11748,20 +12805,27 @@ function resolveBaseUrl(g) {
11748
12805
  return baseUrl.replace(/\/$/, "");
11749
12806
  }
11750
12807
  async function fetchWorkspaces(client) {
11751
- const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
11752
- if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
12808
+ const { data, error } = await client.GET("/workspaces", {
12809
+ params: { query: { includeArchived: false } }
12810
+ });
12811
+ if (error)
12812
+ throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
11753
12813
  const rows = data ?? [];
11754
12814
  return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
11755
12815
  }
11756
12816
  async function lookupWorkspace(client, id) {
11757
- const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
12817
+ const { data, error } = await client.GET("/workspaces/{workspaceId}", {
12818
+ params: { path: { workspaceId: id } }
12819
+ });
11758
12820
  if (error) return null;
11759
12821
  const env = data;
11760
12822
  const w = env?.item ?? env;
11761
12823
  return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
11762
12824
  }
11763
12825
  async function warnIfProjectStray(client, projectId, workspaceId, json) {
11764
- const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
12826
+ const { data, error } = await client.GET("/projects/{projectId}", {
12827
+ params: { path: { projectId } }
12828
+ });
11765
12829
  if (error) return;
11766
12830
  const env = data;
11767
12831
  const owner = env?.item?.workspaceId;
@@ -11805,10 +12869,15 @@ function personalSubtreeIds(personalId, all) {
11805
12869
  async function pickWorkspace(client, opts = {}) {
11806
12870
  const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
11807
12871
  const dirName = opts.dirName ?? basename5(process.cwd());
11808
- const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
12872
+ const all = await withSpinner(
12873
+ "Listing your workspaces",
12874
+ () => fetchWorkspaces(client)
12875
+ );
11809
12876
  if (all.length === 0) {
11810
- process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
11811
- `);
12877
+ process.stderr.write(
12878
+ `no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
12879
+ `
12880
+ );
11812
12881
  return void 0;
11813
12882
  }
11814
12883
  const byId = new Map(all.map((w) => [w.id, w]));
@@ -11823,19 +12892,35 @@ async function pickWorkspace(client, opts = {}) {
11823
12892
  const matched = candidates.filter(isMatch).sort(byPath);
11824
12893
  const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
11825
12894
  const choices = [
11826
- ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
11827
- ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
11828
- { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
12895
+ ...matched.map((w) => ({
12896
+ label: workspacePath(w, byId),
12897
+ value: w.id,
12898
+ hint: style.dim(`matches "${dirName}"`)
12899
+ })),
12900
+ ...rest.map((w) => ({
12901
+ label: workspacePath(w, byId),
12902
+ value: w.id,
12903
+ hint: w.id
12904
+ })),
12905
+ {
12906
+ label: style.dim("skip \u2014 don't bind a workspace"),
12907
+ value: SKIP,
12908
+ hint: void 0
12909
+ }
11829
12910
  ];
11830
12911
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
11831
12912
  const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
11832
12913
  if (chosen === SKIP) return void 0;
11833
12914
  const picked = byId.get(chosen);
11834
- const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
12915
+ const collisions = all.filter(
12916
+ (w) => w.id !== picked.id && namesCollide(w.name, picked.name)
12917
+ );
11835
12918
  if (collisions.length > 0) {
11836
12919
  process.stderr.write(
11837
12920
  `${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
11838
- ` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
12921
+ ` + collisions.map(
12922
+ (w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
12923
+ ).join("\n") + `
11839
12924
  You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
11840
12925
  `
11841
12926
  );
@@ -11862,10 +12947,17 @@ async function ensureTenant(baseUrl, g, opts) {
11862
12947
  const local = readLocalConfig();
11863
12948
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
11864
12949
  if (!tenant) {
11865
- const client = await makeClient({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: persisted.clientId });
12950
+ const client = await makeClient({
12951
+ baseUrl,
12952
+ tenant: "",
12953
+ account: resolveAccountAlias(),
12954
+ clientId: persisted.clientId
12955
+ });
11866
12956
  const { data, error } = await client.GET("/auth/me/tenants", {});
11867
12957
  if (error) {
11868
- fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
12958
+ fail(
12959
+ `Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
12960
+ );
11869
12961
  }
11870
12962
  const tenants = data?.tenants ?? [];
11871
12963
  if (tenants.length === 0) {
@@ -11897,21 +12989,44 @@ async function ensureTenant(baseUrl, g, opts) {
11897
12989
  }
11898
12990
  }
11899
12991
  const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
11900
- const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
11901
- const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
11902
- yes: opts.yes,
11903
- json: opts.json,
11904
- workspace: opts.workspace
12992
+ const wsClient = await makeClient({
12993
+ baseUrl,
12994
+ tenant,
12995
+ account: resolveAccountAlias(),
12996
+ clientId: persisted.clientId
11905
12997
  });
12998
+ const workspaceId = await resolveWorkspaceBinding(
12999
+ wsClient,
13000
+ existingWorkspace,
13001
+ {
13002
+ yes: opts.yes,
13003
+ json: opts.json,
13004
+ workspace: opts.workspace
13005
+ }
13006
+ );
11906
13007
  const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
11907
- if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
13008
+ if (defaultProjectId && workspaceId)
13009
+ await warnIfProjectStray(
13010
+ wsClient,
13011
+ defaultProjectId,
13012
+ workspaceId,
13013
+ opts.json
13014
+ );
11908
13015
  let storeLocal = Boolean(opts.local);
11909
13016
  if (!opts.local && canPrompt() && !opts.yes) {
11910
13017
  storeLocal = await promptSelect(
11911
13018
  "Where should this tenant + base URL be saved?",
11912
13019
  [
11913
- { label: "Globally", value: "global", hint: "all projects on this machine" },
11914
- { label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
13020
+ {
13021
+ label: "Globally",
13022
+ value: "global",
13023
+ hint: "all projects on this machine"
13024
+ },
13025
+ {
13026
+ label: "This directory",
13027
+ value: "local",
13028
+ hint: ".sechroom.json \u2014 committed, project + subdirs"
13029
+ }
11915
13030
  ],
11916
13031
  local.path ? "local" : "global"
11917
13032
  ) === "local";
@@ -11920,19 +13035,34 @@ async function ensureTenant(baseUrl, g, opts) {
11920
13035
  const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
11921
13036
  if (storeLocal) {
11922
13037
  const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
11923
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
11924
- `);
13038
+ if (!opts.json)
13039
+ process.stderr.write(
13040
+ `${ok("\u2713")} config saved to ${path} (directory-local)
13041
+ `
13042
+ );
11925
13043
  } else {
11926
13044
  writePersisted(patch);
11927
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
11928
- `);
13045
+ if (!opts.json)
13046
+ process.stderr.write(
13047
+ `${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
13048
+ `
13049
+ );
11929
13050
  }
11930
13051
  if (workspaceId && !existingWorkspace && !opts.json) {
11931
- process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
11932
- `);
13052
+ process.stderr.write(
13053
+ `${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
13054
+ `
13055
+ );
11933
13056
  }
11934
13057
  }
11935
- return { baseUrl, tenant, account: resolveAccountAlias(), workspaceId, defaultProjectId, clientId: persisted.clientId };
13058
+ return {
13059
+ baseUrl,
13060
+ tenant,
13061
+ account: resolveAccountAlias(),
13062
+ workspaceId,
13063
+ defaultProjectId,
13064
+ clientId: persisted.clientId
13065
+ };
11936
13066
  }
11937
13067
  async function ensureAuth(cfg, yes) {
11938
13068
  if (process.env.SECHROOM_TOKEN) return;
@@ -11940,17 +13070,27 @@ async function ensureAuth(cfg, yes) {
11940
13070
  const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
11941
13071
  if (usable) return;
11942
13072
  if (!canPrompt() || yes) {
11943
- fail("Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use.");
13073
+ fail(
13074
+ "Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
13075
+ );
11944
13076
  }
11945
- process.stderr.write("\nNot signed in \u2014 opening the browser to authenticate.\n");
13077
+ process.stderr.write(
13078
+ "\nNot signed in \u2014 opening the browser to authenticate.\n"
13079
+ );
11946
13080
  await login(cfg);
11947
13081
  }
11948
13082
  async function ensureTimezone(cfg, opts) {
11949
13083
  const client = await makeClient(cfg);
11950
13084
  const { data, error } = await client.GET("/me/profile", {});
11951
- if (error) return { timezone: null, action: "skipped", note: "could not read profile" };
13085
+ if (error)
13086
+ return {
13087
+ timezone: null,
13088
+ action: "skipped",
13089
+ note: "could not read profile"
13090
+ };
11952
13091
  const current = data?.effectiveTimezone;
11953
- if (current && current.trim().length > 0) return { timezone: current, action: "already-set" };
13092
+ if (current && current.trim().length > 0)
13093
+ return { timezone: current, action: "already-set" };
11954
13094
  const system = systemTimezone();
11955
13095
  let tz = system;
11956
13096
  if (canPrompt() && !opts.yes) {
@@ -11962,12 +13102,18 @@ async function ensureTimezone(cfg, opts) {
11962
13102
  note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
11963
13103
  };
11964
13104
  }
11965
- if (!tz) return { timezone: null, action: "skipped", note: "no timezone provided" };
13105
+ if (!tz)
13106
+ return { timezone: null, action: "skipped", note: "no timezone provided" };
11966
13107
  if (opts.dryRun) return { timezone: tz, action: "dry-run" };
11967
13108
  const { error: putErr } = await client.PUT("/me/profile", {
11968
13109
  body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
11969
13110
  });
11970
- if (putErr) return { timezone: tz, action: "skipped", note: `update failed: ${JSON.stringify(putErr)}` };
13111
+ if (putErr)
13112
+ return {
13113
+ timezone: tz,
13114
+ action: "skipped",
13115
+ note: `update failed: ${JSON.stringify(putErr)}`
13116
+ };
11971
13117
  return { timezone: tz, action: "set" };
11972
13118
  }
11973
13119
  async function chooseClients(clientFlag, yes, cwd) {
@@ -11992,7 +13138,11 @@ async function chooseScope(scopeFlag, yes) {
11992
13138
  return promptSelect(
11993
13139
  "Install skills, agents, and hooks globally or just for this project?",
11994
13140
  [
11995
- { label: "Globally", value: "global", hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects" },
13141
+ {
13142
+ label: "Globally",
13143
+ value: "global",
13144
+ hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
13145
+ },
11996
13146
  { label: "This project", value: "project", hint: "<repo>/.claude" }
11997
13147
  ],
11998
13148
  "global"
@@ -12001,7 +13151,13 @@ async function chooseScope(scopeFlag, yes) {
12001
13151
  async function planRecurseChild(entry, root, client, opts) {
12002
13152
  const dir = resolveChildDir(entry.path, root);
12003
13153
  if (!existsSync15(dir)) {
12004
- return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
13154
+ return {
13155
+ label: entry.path,
13156
+ dir,
13157
+ disposition: "skip-missing",
13158
+ argv: [],
13159
+ reason: "directory does not exist"
13160
+ };
12005
13161
  }
12006
13162
  if (existsSync15(join21(dir, ".sechroom.json"))) {
12007
13163
  return {
@@ -12022,20 +13178,40 @@ async function planRecurseChild(entry, root, client, opts) {
12022
13178
  };
12023
13179
  }
12024
13180
  if (opts.dryRun) {
12025
- return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
13181
+ return {
13182
+ label: entry.path,
13183
+ dir,
13184
+ disposition: "bind",
13185
+ argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
13186
+ reason: "unbound \u2014 would prompt for a workspace"
13187
+ };
12026
13188
  }
12027
13189
  if (opts.yes || !canPrompt()) {
12028
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
13190
+ return {
13191
+ label: entry.path,
13192
+ dir,
13193
+ disposition: "skip-unbound",
13194
+ argv: [],
13195
+ reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
13196
+ };
12029
13197
  }
12030
- process.stderr.write(`
13198
+ process.stderr.write(
13199
+ `
12031
13200
  ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
12032
- `);
13201
+ `
13202
+ );
12033
13203
  const ws = await pickWorkspace(client, {
12034
13204
  promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
12035
13205
  dirName: basename5(entry.path)
12036
13206
  });
12037
13207
  if (!ws) {
12038
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
13208
+ return {
13209
+ label: entry.path,
13210
+ dir,
13211
+ disposition: "skip-unbound",
13212
+ argv: [],
13213
+ reason: "unbound \u2014 no workspace chosen (skipped)"
13214
+ };
12039
13215
  }
12040
13216
  return {
12041
13217
  label: entry.path,
@@ -12050,20 +13226,34 @@ async function resolveFanoutLane(cfg, opts) {
12050
13226
  let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
12051
13227
  if (!code || !design) {
12052
13228
  const clients = detectInstalledClients(process.cwd());
12053
- const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
13229
+ const inferred = await inferLanes(
13230
+ cfg,
13231
+ clients.length ? clients : void 0
13232
+ );
12054
13233
  code = code ?? inferred.code;
12055
13234
  design = design ?? inferred.design;
12056
13235
  }
12057
13236
  if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
12058
- process.stderr.write(`
13237
+ process.stderr.write(
13238
+ `
12059
13239
  This fan-out will pin the same lane in every repo:
12060
- `);
12061
- if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
12062
- `);
12063
- if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
12064
- `);
13240
+ `
13241
+ );
13242
+ if (code)
13243
+ process.stderr.write(
13244
+ ` ${style.dim("code-lane")} = ${style.cyan(code)}
13245
+ `
13246
+ );
13247
+ if (design)
13248
+ process.stderr.write(
13249
+ ` ${style.dim("design-lane")} = ${style.cyan(design)}
13250
+ `
13251
+ );
12065
13252
  if (!await promptYesNo("Use this lane for all repos?")) {
12066
- code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
13253
+ code = await promptText(
13254
+ "Code-lane id (blank = let each repo infer)?",
13255
+ code ?? ""
13256
+ ) || void 0;
12067
13257
  design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
12068
13258
  }
12069
13259
  }
@@ -12081,30 +13271,112 @@ async function runRecurse(cfg, g, opts) {
12081
13271
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
12082
13272
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
12083
13273
  if (entries.length === 0) {
12084
- if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
12085
- else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
12086
- `);
13274
+ if (json)
13275
+ process.stdout.write(
13276
+ JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
13277
+ );
13278
+ else
13279
+ process.stderr.write(
13280
+ `${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
13281
+ `
13282
+ );
12087
13283
  return;
12088
13284
  }
12089
13285
  if (!json) {
12090
- process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12091
- `);
13286
+ process.stderr.write(
13287
+ `${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
13288
+ `
13289
+ );
12092
13290
  }
12093
- const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
12094
- if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
12095
- `);
13291
+ const lane = await resolveFanoutLane(cfg, {
13292
+ lane: opts.lane,
13293
+ designLane: opts.designLane,
13294
+ yes,
13295
+ dryRun
13296
+ });
13297
+ if (!json && lane.code)
13298
+ process.stderr.write(
13299
+ `${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
13300
+ `
13301
+ );
12096
13302
  const client = await makeClient(cfg);
12097
13303
  const plans = [];
12098
- for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
12099
- const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
13304
+ for (const entry of entries)
13305
+ plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
13306
+ const results = runChildren(plans, {
13307
+ globals: passthroughGlobals(g),
13308
+ dryRun,
13309
+ json
13310
+ });
12100
13311
  if (json) {
12101
- process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
13312
+ process.stdout.write(
13313
+ JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
13314
+ );
12102
13315
  return;
12103
13316
  }
12104
13317
  summarizeFanout(results, { dryRun });
12105
13318
  }
12106
13319
  function registerOnboard(program2) {
12107
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global").option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
13320
+ program2.command("onboard").description(
13321
+ "Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
13322
+ ).option(
13323
+ "--recurse",
13324
+ "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one",
13325
+ false
13326
+ ).option(
13327
+ "--lane <id>",
13328
+ "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
13329
+ ).option(
13330
+ "--design-lane <id>",
13331
+ "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
13332
+ ).option(
13333
+ "--client <list...>",
13334
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
13335
+ ).option(
13336
+ "--scope <scope>",
13337
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
13338
+ ).option(
13339
+ "--local",
13340
+ "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
13341
+ false
13342
+ ).option(
13343
+ "--here",
13344
+ "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace",
13345
+ false
13346
+ ).option(
13347
+ "--workspace <id>",
13348
+ "bind this directory to a workspace (skips the interactive workspace pick)"
13349
+ ).option(
13350
+ "--cli-only",
13351
+ "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
13352
+ false
13353
+ ).option(
13354
+ "--no-mcp",
13355
+ "skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
13356
+ ).option(
13357
+ "--copy",
13358
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
13359
+ ).option(
13360
+ "--dry-run",
13361
+ "walk through without writing files or changing the profile",
13362
+ false
13363
+ ).option(
13364
+ "--refresh",
13365
+ "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
13366
+ false
13367
+ ).option(
13368
+ "--force",
13369
+ "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
13370
+ false
13371
+ ).option(
13372
+ "--check",
13373
+ "report whether anything would change and exit (0 = current/stub or unresolved warning, 1 = stale/drift/absent); writes nothing",
13374
+ false
13375
+ ).option(
13376
+ "-y, --yes",
13377
+ "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
13378
+ false
13379
+ ).addHelpText(
12108
13380
  "after",
12109
13381
  `
12110
13382
  Examples:
@@ -12127,20 +13399,56 @@ Examples:
12127
13399
  const check = mode === "check";
12128
13400
  const yes = Boolean(opts.yes) || check;
12129
13401
  if (check && (opts.recurse || opts.cliOnly)) {
12130
- fail("--check inspects this project's agent files and cannot be combined with --recurse or --cli-only.");
13402
+ fail(
13403
+ "--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
13404
+ );
12131
13405
  }
12132
13406
  if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
12133
13407
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
12134
13408
  if (opts.recurse) {
12135
13409
  const baseUrl2 = resolveBaseUrl(g);
12136
- await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12137
- const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
12138
- await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
13410
+ await ensureAuth(
13411
+ {
13412
+ baseUrl: baseUrl2,
13413
+ tenant: "",
13414
+ account: resolveAccountAlias(),
13415
+ clientId: readPersisted().clientId
13416
+ },
13417
+ yes
13418
+ );
13419
+ const cfg2 = await ensureTenant(baseUrl2, g, {
13420
+ yes: true,
13421
+ json,
13422
+ persist: false
13423
+ });
13424
+ await runRecurse(cfg2, g, {
13425
+ yes,
13426
+ dryRun,
13427
+ json,
13428
+ lane: opts.lane,
13429
+ designLane: opts.designLane
13430
+ });
12139
13431
  return;
12140
13432
  }
12141
13433
  const baseUrl = resolveBaseUrl(g);
12142
- await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12143
- const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
13434
+ await ensureAuth(
13435
+ {
13436
+ baseUrl,
13437
+ tenant: "",
13438
+ account: resolveAccountAlias(),
13439
+ clientId: readPersisted().clientId
13440
+ },
13441
+ yes
13442
+ );
13443
+ const scope = await chooseScope(opts.scope, yes);
13444
+ const cfg = await ensureTenant(baseUrl, g, {
13445
+ yes,
13446
+ json,
13447
+ local: Boolean(opts.local) || scope === "project",
13448
+ here: scope === "project" ? true : Boolean(opts.here),
13449
+ workspace: opts.workspace,
13450
+ persist: !check
13451
+ });
12144
13452
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
12145
13453
  if (!json && tz.action !== "already-set") {
12146
13454
  const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
@@ -12150,21 +13458,48 @@ Examples:
12150
13458
  process.stderr.write(line);
12151
13459
  }
12152
13460
  const wire = await chooseWire(opts, yes);
12153
- const scope = await chooseScope(opts.scope, yes);
12154
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
13461
+ const claudeTargets = resolveClaudeTargets({
13462
+ override: g.claudeConfigDir,
13463
+ scope,
13464
+ cwd: process.cwd()
13465
+ });
12155
13466
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
12156
13467
  if (scope === "project" && g.claudeConfigDir && !json) {
12157
- process.stderr.write(`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
12158
- `);
13468
+ process.stderr.write(
13469
+ `${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
13470
+ `
13471
+ );
12159
13472
  }
12160
13473
  if (wire === "cli-only") {
12161
13474
  if (json) {
12162
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
13475
+ emit(
13476
+ {
13477
+ dryRun,
13478
+ baseUrl: cfg.baseUrl,
13479
+ tenant: cfg.tenant,
13480
+ workspaceId: cfg.workspaceId ?? null,
13481
+ timezone: tz,
13482
+ wire,
13483
+ clients: []
13484
+ },
13485
+ true
13486
+ );
12163
13487
  return;
12164
13488
  }
12165
13489
  if (!dryRun) {
12166
- await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
12167
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13490
+ await ensureLanePin(cfg, {
13491
+ yes,
13492
+ dryRun,
13493
+ clients: detectInstalledClients(process.cwd())
13494
+ });
13495
+ await maybeOfferHooks({
13496
+ yes,
13497
+ dryRun,
13498
+ cwd: process.cwd(),
13499
+ scope,
13500
+ claudeConfigDir: g.claudeConfigDir,
13501
+ codexHome: g.codexHome
13502
+ });
12168
13503
  }
12169
13504
  process.stdout.write(
12170
13505
  `
@@ -12175,12 +13510,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12175
13510
  await printStarterPrompt("cli");
12176
13511
  return;
12177
13512
  }
12178
- const keys = await chooseClients(opts.client, yes, process.cwd());
12179
- const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
12180
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
13513
+ const requestedKeys = await chooseClients(
13514
+ opts.client,
13515
+ yes,
13516
+ process.cwd()
13517
+ );
13518
+ const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
13519
+ if (scope === "project" && requestedKeys.includes("codex") && !json) {
13520
+ process.stderr.write(
13521
+ `${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
13522
+ `
13523
+ );
13524
+ }
13525
+ const setup = await withSpinner(
13526
+ "Fetching setup descriptors",
13527
+ () => fetchSetup(cfg)
13528
+ );
13529
+ const targets = clientTargets(process.cwd(), {
13530
+ claudeDir: claudeTargets[0]?.dir,
13531
+ codexHome: codexHomes[0] ?? null
13532
+ });
12181
13533
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
12182
13534
  if (!dryRun && !check) {
12183
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
13535
+ await maybeOfferCopies(
13536
+ cfg,
13537
+ setup,
13538
+ targets,
13539
+ keys,
13540
+ personalWorkspaceId,
13541
+ copyChoice(opts)
13542
+ );
12184
13543
  }
12185
13544
  const writeMcp = wire === "full";
12186
13545
  const result = [];
@@ -12197,16 +13556,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12197
13556
  if (!json && !check) printActions(target, actions);
12198
13557
  }
12199
13558
  if (check) {
12200
- reportCheckAndExit(
12201
- result,
12202
- json,
12203
- "sechroom onboard --refresh",
12204
- {
12205
- baseUrl: cfg.baseUrl,
12206
- tenant: cfg.tenant,
12207
- workspaceId: cfg.workspaceId ?? null
12208
- }
12209
- );
13559
+ reportCheckAndExit(result, json, "sechroom onboard --refresh", {
13560
+ baseUrl: cfg.baseUrl,
13561
+ tenant: cfg.tenant,
13562
+ workspaceId: cfg.workspaceId ?? null
13563
+ });
12210
13564
  }
12211
13565
  const evalCounts = buildCheckReport(result).eval;
12212
13566
  if (!json && !dryRun) {
@@ -12214,19 +13568,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12214
13568
  }
12215
13569
  if (!json && !dryRun) {
12216
13570
  for (const t of claudeTargets) {
12217
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code", configDir: t.dir });
13571
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
13572
+ yes,
13573
+ dryRun,
13574
+ surface: "claude-code",
13575
+ configDir: t.dir
13576
+ });
12218
13577
  }
12219
13578
  }
12220
13579
  if (!json && !dryRun) {
12221
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13580
+ await maybeOfferHooks({
13581
+ yes,
13582
+ dryRun,
13583
+ cwd: process.cwd(),
13584
+ scope,
13585
+ claudeConfigDir: g.claudeConfigDir,
13586
+ codexHome: g.codexHome
13587
+ });
12222
13588
  }
12223
13589
  if (json) {
12224
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
13590
+ emit(
13591
+ {
13592
+ dryRun,
13593
+ baseUrl: cfg.baseUrl,
13594
+ tenant: cfg.tenant,
13595
+ workspaceId: cfg.workspaceId ?? null,
13596
+ timezone: tz,
13597
+ wire,
13598
+ eval: evalCounts,
13599
+ clients: result
13600
+ },
13601
+ true
13602
+ );
12225
13603
  return;
12226
13604
  }
12227
13605
  if (!dryRun && evalCounts.stale) {
12228
- process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
12229
- `);
13606
+ process.stderr.write(
13607
+ `${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
13608
+ `
13609
+ );
12230
13610
  }
12231
13611
  if (!dryRun && evalCounts.drift) {
12232
13612
  process.stderr.write(
@@ -12235,7 +13615,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12235
13615
  `
12236
13616
  );
12237
13617
  }
12238
- const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
13618
+ const wroteSomething = result.some(
13619
+ ({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
13620
+ );
12239
13621
  process.stdout.write(
12240
13622
  dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
12241
13623
  ${style.bold("Done.")} Everything's already up to date.
@@ -12254,9 +13636,21 @@ async function chooseWire(opts, yes) {
12254
13636
  return promptSelect(
12255
13637
  "How should I set up Sechroom in this project?",
12256
13638
  [
12257
- { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
12258
- { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
12259
- { label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
13639
+ {
13640
+ label: "Wire my AI client",
13641
+ value: "full",
13642
+ hint: "MCP server (.mcp.json) + agent instructions"
13643
+ },
13644
+ {
13645
+ label: "Agent instructions only",
13646
+ value: "agent-only",
13647
+ hint: "skip MCP config"
13648
+ },
13649
+ {
13650
+ label: "CLI only",
13651
+ value: "cli-only",
13652
+ hint: "don't write any AI-client files"
13653
+ }
12260
13654
  ],
12261
13655
  "full"
12262
13656
  );
@@ -12278,7 +13672,9 @@ ${rule}
12278
13672
  }
12279
13673
  async function printStarterPrompt(mode, cfg) {
12280
13674
  if (mode === "cli") {
12281
- printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
13675
+ printNextStepBlock("Next \u2014 pick up where you left off:", [
13676
+ style.cyan("sechroom continuity resume-me")
13677
+ ]);
12282
13678
  return;
12283
13679
  }
12284
13680
  let primary = FALLBACK_AGENT_PROMPT;
@@ -12290,7 +13686,9 @@ async function printStarterPrompt(mode, cfg) {
12290
13686
  } catch {
12291
13687
  }
12292
13688
  }
12293
- printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
13689
+ printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
13690
+ style.cyan(`"${primary}"`)
13691
+ ]);
12294
13692
  }
12295
13693
 
12296
13694
  // src/commands/project.ts
@@ -12443,7 +13841,13 @@ Examples:
12443
13841
  $ sechroom relationship suggestions --status Pending --memory mem_XXXX
12444
13842
  $ sechroom relationship suggestion accept rsg_XXXX`
12445
13843
  );
12446
- relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
13844
+ relationship.command("create <fromMemoryId> <toMemoryId>").description(
13845
+ "Create a relationship (POST /memories/{memoryId}/relationships)"
13846
+ ).option(
13847
+ "--type <type>",
13848
+ "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)",
13849
+ "Reference"
13850
+ ).option(
12447
13851
  "--to-version <number>",
12448
13852
  "Version of the target memory to pin the edge to (defaults to the target's current version)"
12449
13853
  ).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
@@ -12453,15 +13857,21 @@ Examples:
12453
13857
  if (opts.toVersion !== void 0) {
12454
13858
  toVersion = Number(opts.toVersion);
12455
13859
  if (!Number.isInteger(toVersion) || toVersion < 1) {
12456
- fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
13860
+ fail(
13861
+ `--to-version must be a positive integer (got '${opts.toVersion}').`
13862
+ );
12457
13863
  }
12458
13864
  } else {
12459
13865
  const target = await runApi(
12460
13866
  "Resolving target version",
12461
- async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
13867
+ async () => client.GET("/memories/{memoryId}", {
13868
+ params: { path: { memoryId: toMemoryId } }
13869
+ })
12462
13870
  );
12463
13871
  if (typeof target.item?.currentVersion !== "number") {
12464
- fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
13872
+ fail(
13873
+ `Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`
13874
+ );
12465
13875
  }
12466
13876
  toVersion = target.item.currentVersion;
12467
13877
  }
@@ -12485,22 +13895,28 @@ Examples:
12485
13895
  cmd.optsWithGlobals().json
12486
13896
  );
12487
13897
  });
12488
- relationship.command("list <memoryId>").description("List a memory's relationships (GET /memories/{memoryId}/relationships)").option("--direction <direction>", "Both | Outbound | Inbound").option("--include-deleted", "Include deleted relationships", false).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (memoryId, opts, cmd) => {
13898
+ relationship.command("list <memoryId>").description(
13899
+ "List a memory's relationships (GET /memories/{memoryId}/relationships). Walks every page by default."
13900
+ ).option("--direction <direction>", "Both | Outbound | Inbound").option("--include-deleted", "Include deleted relationships", false).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (memoryId, opts, cmd) => {
12489
13901
  const cfg = resolveConfig(cmd.optsWithGlobals());
12490
- const data = await runApi("Listing relationships", async () => {
13902
+ const filters = {
13903
+ ...opts.direction ? { direction: opts.direction } : {},
13904
+ ...opts.includeDeleted ? { includeDeleted: true } : {}
13905
+ };
13906
+ const readPage = (query) => runApi("Listing relationships", async () => {
12491
13907
  const client = await makeClient(cfg);
12492
13908
  return client.GET("/memories/{memoryId}/relationships", {
12493
- params: {
12494
- path: { memoryId },
12495
- query: {
12496
- ...opts.direction ? { direction: opts.direction } : {},
12497
- ...opts.includeDeleted ? { includeDeleted: true } : {},
12498
- ...opts.page ? { page: Number(opts.page) } : {},
12499
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
12500
- }
12501
- }
13909
+ params: { path: { memoryId }, query: { ...filters, ...query } }
12502
13910
  });
12503
13911
  });
13912
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
13913
+ (page) => readPage({
13914
+ page,
13915
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
13916
+ }),
13917
+ opts,
13918
+ "relationships"
13919
+ ) : await readPage(singlePageQuery(opts));
12504
13920
  emit(data, cmd.optsWithGlobals().json);
12505
13921
  });
12506
13922
  relationship.command("delete <id>").description("Delete a relationship (DELETE /relationships/{id})").action(async (id, _opts, cmd) => {
@@ -12512,9 +13928,15 @@ Examples:
12512
13928
  body: {}
12513
13929
  });
12514
13930
  });
12515
- emitAction(`deleted relationship ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
13931
+ emitAction(
13932
+ `deleted relationship ${style.bold(id)}`,
13933
+ data,
13934
+ cmd.optsWithGlobals().json
13935
+ );
12516
13936
  });
12517
- relationship.command("suggest <memoryId>").description("Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)").option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
13937
+ relationship.command("suggest <memoryId>").description(
13938
+ "Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)"
13939
+ ).option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
12518
13940
  const cfg = resolveConfig(cmd.optsWithGlobals());
12519
13941
  const data = await runApi("Suggesting relationships", async () => {
12520
13942
  const client = await makeClient(cfg);
@@ -12531,38 +13953,51 @@ Examples:
12531
13953
  cmd.optsWithGlobals().json
12532
13954
  );
12533
13955
  });
12534
- relationship.command("suggestions").description("List relationship suggestions (GET /relationship-suggestions)").option("--memory <memoryId>", "Filter to a memory").option(
13956
+ relationship.command("suggestions").description(
13957
+ "List relationship suggestions (GET /relationship-suggestions)"
13958
+ ).option("--memory <memoryId>", "Filter to a memory").option(
12535
13959
  "--status <status>",
12536
13960
  "Pending | Accepted | EditedAndAccepted | Rejected | Superseded | Deferred | Invalidated"
12537
- ).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
13961
+ ).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
12538
13962
  const cfg = resolveConfig(cmd.optsWithGlobals());
12539
- const data = await runApi("Listing suggestions", async () => {
13963
+ const filters = {
13964
+ ...opts.memory ? { memoryId: opts.memory } : {},
13965
+ ...opts.status ? {
13966
+ status: opts.status
13967
+ } : {}
13968
+ };
13969
+ const readPage = (query) => runApi("Listing suggestions", async () => {
12540
13970
  const client = await makeClient(cfg);
12541
13971
  return client.GET("/relationship-suggestions", {
12542
- params: {
12543
- query: {
12544
- ...opts.memory ? { memoryId: opts.memory } : {},
12545
- ...opts.status ? {
12546
- status: opts.status
12547
- } : {},
12548
- ...opts.page ? { page: Number(opts.page) } : {},
12549
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
12550
- }
12551
- }
13972
+ params: { query: { ...filters, ...query } }
12552
13973
  });
12553
13974
  });
13975
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
13976
+ (page) => readPage({
13977
+ page,
13978
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
13979
+ }),
13980
+ opts,
13981
+ "suggestions"
13982
+ ) : await readPage(singlePageQuery(opts));
12554
13983
  emit(data, cmd.optsWithGlobals().json);
12555
13984
  });
12556
13985
  const suggestion = relationship.command("suggestion").description("Inspect and decide on a single relationship suggestion");
12557
- suggestion.command("get <id>").description("Fetch a suggestion by id (GET /relationship-suggestions/{id})").action(async (id, _opts, cmd) => {
13986
+ suggestion.command("get <id>").description(
13987
+ "Fetch a suggestion by id (GET /relationship-suggestions/{id})"
13988
+ ).action(async (id, _opts, cmd) => {
12558
13989
  const cfg = resolveConfig(cmd.optsWithGlobals());
12559
13990
  const data = await runApi("Fetching suggestion", async () => {
12560
13991
  const client = await makeClient(cfg);
12561
- return client.GET("/relationship-suggestions/{id}", { params: { path: { id } } });
13992
+ return client.GET("/relationship-suggestions/{id}", {
13993
+ params: { path: { id } }
13994
+ });
12562
13995
  });
12563
13996
  emit(data, cmd.optsWithGlobals().json);
12564
13997
  });
12565
- suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)").action(async (id, _opts, cmd) => {
13998
+ suggestion.command("accept <id>").description(
13999
+ "Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)"
14000
+ ).action(async (id, _opts, cmd) => {
12566
14001
  const cfg = resolveConfig(cmd.optsWithGlobals());
12567
14002
  const data = await runApi("Accepting suggestion", async () => {
12568
14003
  const client = await makeClient(cfg);
@@ -12571,9 +14006,15 @@ Examples:
12571
14006
  body: {}
12572
14007
  });
12573
14008
  });
12574
- emitAction(`accepted suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
14009
+ emitAction(
14010
+ `accepted suggestion ${style.bold(id)}`,
14011
+ data,
14012
+ cmd.optsWithGlobals().json
14013
+ );
12575
14014
  });
12576
- suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
14015
+ suggestion.command("reject <id>").description(
14016
+ "Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)"
14017
+ ).option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
12577
14018
  const cfg = resolveConfig(cmd.optsWithGlobals());
12578
14019
  const data = await runApi("Rejecting suggestion", async () => {
12579
14020
  const client = await makeClient(cfg);
@@ -12585,9 +14026,18 @@ Examples:
12585
14026
  }
12586
14027
  });
12587
14028
  });
12588
- emitAction(`rejected suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
14029
+ emitAction(
14030
+ `rejected suggestion ${style.bold(id)}`,
14031
+ data,
14032
+ cmd.optsWithGlobals().json
14033
+ );
12589
14034
  });
12590
- suggestion.command("defer <id>").description("Defer a suggestion (POST /relationship-suggestions/{id}/defer)").option("--until <iso>", "Defer until this ISO date-time (omit to defer indefinitely)").action(async (id, opts, cmd) => {
14035
+ suggestion.command("defer <id>").description(
14036
+ "Defer a suggestion (POST /relationship-suggestions/{id}/defer)"
14037
+ ).option(
14038
+ "--until <iso>",
14039
+ "Defer until this ISO date-time (omit to defer indefinitely)"
14040
+ ).action(async (id, opts, cmd) => {
12591
14041
  const cfg = resolveConfig(cmd.optsWithGlobals());
12592
14042
  const data = await runApi("Deferring suggestion", async () => {
12593
14043
  const client = await makeClient(cfg);
@@ -12598,14 +14048,18 @@ Examples:
12598
14048
  }
12599
14049
  });
12600
14050
  });
12601
- emitAction(`deferred suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
14051
+ emitAction(
14052
+ `deferred suggestion ${style.bold(id)}`,
14053
+ data,
14054
+ cmd.optsWithGlobals().json
14055
+ );
12602
14056
  });
12603
14057
  }
12604
14058
 
12605
14059
  // src/commands/reset.ts
12606
14060
  import { homedir as homedir6 } from "os";
12607
14061
  import { join as join22 } from "path";
12608
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
14062
+ import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
12609
14063
  var SKILLS_LOCK2 = ".sechroom-skills.json";
12610
14064
  var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
12611
14065
  var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
@@ -12621,14 +14075,14 @@ function removeMaterialisedSkills(dir) {
12621
14075
  for (const name of entry.skills ?? []) {
12622
14076
  const p = join22(dir, name);
12623
14077
  if (existsSync16(p)) {
12624
- rmSync7(p, { recursive: true, force: true });
14078
+ rmSync8(p, { recursive: true, force: true });
12625
14079
  removed.push(p);
12626
14080
  }
12627
14081
  }
12628
14082
  }
12629
14083
  } catch {
12630
14084
  }
12631
- rmSync7(lockPath, { force: true });
14085
+ rmSync8(lockPath, { force: true });
12632
14086
  removed.push(lockPath);
12633
14087
  return removed;
12634
14088
  }
@@ -12667,17 +14121,17 @@ function registerReset(program2) {
12667
14121
  const removed = [];
12668
14122
  const stateDir = join22(process.cwd(), ".sechroom");
12669
14123
  if (existsSync16(stateDir)) {
12670
- rmSync7(stateDir, { recursive: true, force: true });
14124
+ rmSync8(stateDir, { recursive: true, force: true });
12671
14125
  removed.push(stateDir);
12672
14126
  }
12673
14127
  const legacyCfg = join22(process.cwd(), ".sechroom.json");
12674
14128
  if (existsSync16(legacyCfg)) {
12675
- rmSync7(legacyCfg, { force: true });
14129
+ rmSync8(legacyCfg, { force: true });
12676
14130
  removed.push(legacyCfg);
12677
14131
  }
12678
14132
  const legacySem = join22(process.cwd(), ".sem");
12679
14133
  if (existsSync16(legacySem)) {
12680
- rmSync7(legacySem, { force: true });
14134
+ rmSync8(legacySem, { force: true });
12681
14135
  removed.push(legacySem);
12682
14136
  }
12683
14137
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -12702,7 +14156,7 @@ function registerReset(program2) {
12702
14156
  }
12703
14157
 
12704
14158
  // src/commands/skills.ts
12705
- import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync6, writeFileSync as writeFileSync17 } from "fs";
14159
+ import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
12706
14160
  import { join as join23 } from "path";
12707
14161
  function filenameFromDisposition(header) {
12708
14162
  if (!header) return void 0;
@@ -12712,7 +14166,7 @@ function filenameFromDisposition(header) {
12712
14166
  function resolveOutputPath(output, serverFilename) {
12713
14167
  const filename = serverFilename || "skills.zip";
12714
14168
  if (!output) return join23(process.cwd(), filename);
12715
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync6(output).isDirectory();
14169
+ const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
12716
14170
  if (looksLikeDir) {
12717
14171
  mkdirSync18(output, { recursive: true });
12718
14172
  return join23(output, filename);
@@ -13311,17 +14765,15 @@ Examples:
13311
14765
  $ sechroom work-task mark-no-residue mem_XXXX --decomposition wlp_XXXX
13312
14766
  $ sechroom work-task residue-produce mem_XXXX --file residue.json`
13313
14767
  );
13314
- workTask.command("list").description("List work tasks, newest-first (GET /work-tasks)").option("--shape <shape>", "Filter: bare | managed").option(
14768
+ workTask.command("list").description(
14769
+ "List work tasks, newest-first (GET /work-tasks). Walks every page by default."
14770
+ ).option("--shape <shape>", "Filter: bare | managed").option(
13315
14771
  "--lane <lane>",
13316
14772
  "Filter by dispatch-lane value, e.g. claude-code-chris"
13317
- ).option("--status <status>", "Filter by status value, e.g. in-progress").option("--page <n>", "1-based page number", (v) => Number.parseInt(v, 10)).option(
13318
- "--page-size <n>",
13319
- "Page size (default 50, capped 200)",
13320
- (v) => Number.parseInt(v, 10)
13321
- ).action(async (opts, cmd) => {
14773
+ ).option("--status <status>", "Filter by status value, e.g. in-progress").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
13322
14774
  const globals = cmd.optsWithGlobals();
13323
14775
  const cfg = resolveConfig(globals);
13324
- const data = await runApi("Listing work tasks", async () => {
14776
+ const readPage = (query) => runApi("Listing work tasks", async () => {
13325
14777
  const client = await makeClient(cfg);
13326
14778
  return client.GET("/work-tasks", {
13327
14779
  params: {
@@ -13329,12 +14781,19 @@ Examples:
13329
14781
  shape: opts.shape,
13330
14782
  lane: opts.lane,
13331
14783
  status: opts.status,
13332
- page: opts.page,
13333
- pageSize: opts.pageSize
14784
+ ...query
13334
14785
  }
13335
14786
  }
13336
14787
  });
13337
14788
  });
14789
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
14790
+ (page) => readPage({
14791
+ page,
14792
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
14793
+ }),
14794
+ opts,
14795
+ "tasks"
14796
+ ) : await readPage(singlePageQuery(opts));
13338
14797
  emitAction(
13339
14798
  `listed ${style.bold(String(data.items.length))} of ${data.count} task(s)`,
13340
14799
  data,