@sechroom/cli 2026.9.1 → 2026.9.3

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 +425 -157
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1234,7 +1234,7 @@ ${mem.text.trim()}`
1234
1234
  if (parts.length === 0) return null;
1235
1235
  return { body: parts.join("\n\n"), refs };
1236
1236
  }
1237
- async function createOverride(cfg, template, personalWorkspaceId) {
1237
+ async function createOverride(cfg, template, personalWorkspaceId, source) {
1238
1238
  const client = await makeClient(cfg);
1239
1239
  const overrideTags = template.templateTags.filter(
1240
1240
  (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
@@ -1249,7 +1249,7 @@ async function createOverride(cfg, template, personalWorkspaceId) {
1249
1249
  type: "reference",
1250
1250
  content: "{}",
1251
1251
  confidence: 1,
1252
- source: "cli-agent-instructions-customize",
1252
+ source,
1253
1253
  archetype: "Document",
1254
1254
  title: template.title ?? null,
1255
1255
  tags: overrideTags,
@@ -2260,6 +2260,7 @@ function resolveSemPathForRead(start = process.cwd()) {
2260
2260
  while (true) {
2261
2261
  const candidate = join6(dir, SEM_FILE);
2262
2262
  if (existsSync4(candidate)) return candidate;
2263
+ if (existsSync4(join6(dir, ".git"))) return void 0;
2263
2264
  const parent = dirname2(dir);
2264
2265
  if (parent === dir) return void 0;
2265
2266
  dir = parent;
@@ -7031,9 +7032,11 @@ function registerChannel(program2) {
7031
7032
  (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
7032
7033
  )
7033
7034
  );
7035
+ const authExit = createAuthExpiredExit();
7034
7036
  const leaseHeartbeats = createChannelLeaseHeartbeatManager(
7035
7037
  cfg,
7036
- located.state.taskLeaseTtlSeconds ?? 120
7038
+ located.state.taskLeaseTtlSeconds ?? 120,
7039
+ { onAuthExpired: authExit.onAuthExpired }
7037
7040
  );
7038
7041
  const drain = createClaimDrain(cfg, instance.id, deliver, {
7039
7042
  onClaimed: leaseHeartbeats.onClaimed
@@ -7072,13 +7075,11 @@ function registerChannel(program2) {
7072
7075
  ) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
7073
7076
  );
7074
7077
  }
7075
- try {
7076
- await holdOpen(conn);
7077
- } finally {
7078
- stopReconciliation();
7079
- stopHeartbeat();
7080
- leaseHeartbeats.stop();
7081
- }
7078
+ await holdChannelOpen(conn, authExit.released, [
7079
+ stopReconciliation,
7080
+ stopHeartbeat,
7081
+ leaseHeartbeats.stop
7082
+ ]);
7082
7083
  });
7083
7084
  channel.command("mcp").description(
7084
7085
  "Run as a Claude Code channel (local-stdio MCP server) \u2014 claim and push dispatched tasks into the session"
@@ -7101,9 +7102,11 @@ function registerChannel(program2) {
7101
7102
  params: { content, meta }
7102
7103
  });
7103
7104
  });
7105
+ const authExit = createAuthExpiredExit();
7104
7106
  const leaseHeartbeats = createChannelLeaseHeartbeatManager(
7105
7107
  cfg,
7106
- located.state.taskLeaseTtlSeconds ?? 120
7108
+ located.state.taskLeaseTtlSeconds ?? 120,
7109
+ { onAuthExpired: authExit.onAuthExpired }
7107
7110
  );
7108
7111
  const drain = createClaimDrain(cfg, instance.id, deliver, {
7109
7112
  onClaimed: leaseHeartbeats.onClaimed
@@ -7129,13 +7132,16 @@ function registerChannel(program2) {
7129
7132
  `
7130
7133
  )
7131
7134
  );
7132
- try {
7133
- await holdOpen(conn);
7134
- } finally {
7135
- stopReconciliation();
7136
- stopHeartbeat();
7137
- leaseHeartbeats.stop();
7138
- }
7135
+ await holdChannelOpen(conn, authExit.released, [
7136
+ stopReconciliation,
7137
+ stopHeartbeat,
7138
+ leaseHeartbeats.stop,
7139
+ // The stdio transport keeps a stdin `data` listener, which holds the event
7140
+ // loop open for as long as the parent holds the pipe. Without this close, a
7141
+ // `process.exitCode` set on the auth path never becomes an exit and the
7142
+ // parent sees a connected-but-inert channel instead of a dead one.
7143
+ () => mcp.close()
7144
+ ]);
7139
7145
  });
7140
7146
  channel.command("install").description(
7141
7147
  "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
@@ -7239,6 +7245,7 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
7239
7245
  }
7240
7246
  var leaseHeartbeatApi = (cfg, path, init, deps = {}) => createAuthedRequest(cfg, deps)(path, init);
7241
7247
  function classifyLeaseHeartbeatFailure(error) {
7248
+ if (error instanceof AuthExpiredError) return "auth";
7242
7249
  if (!(error instanceof HttpError)) return "retry";
7243
7250
  if (error.status === 408 || error.status === 429) return "retry";
7244
7251
  if (error.status < 400 || error.status >= 500) return "retry";
@@ -7246,6 +7253,10 @@ function classifyLeaseHeartbeatFailure(error) {
7246
7253
  return "released";
7247
7254
  return "terminal";
7248
7255
  }
7256
+ function authExpiredLine(error) {
7257
+ const detail = error instanceof Error ? error.message : String(error);
7258
+ return `auth expired \u2014 run \`sechroom login\` to re-authenticate: ${detail}`;
7259
+ }
7249
7260
  function leaseHeartbeatStoppedLine(leaseId, error) {
7250
7261
  if (!(error instanceof HttpError))
7251
7262
  return `lease heartbeat stopped for ${leaseId}: ${String(error)}`;
@@ -7288,6 +7299,10 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
7288
7299
  if (disposition === "retry") throw error;
7289
7300
  stop?.();
7290
7301
  dependencies.onLeaseTerminal?.(leaseId, error);
7302
+ if (disposition === "auth") {
7303
+ dependencies.onAuthExpired?.(error);
7304
+ return void 0;
7305
+ }
7291
7306
  if (disposition === "terminal")
7292
7307
  onError(leaseHeartbeatStoppedLine(leaseId, error));
7293
7308
  return void 0;
@@ -7301,6 +7316,7 @@ function startChannelTaskLeaseHeartbeat(cfg, claim, intervalMilliseconds = 3e4,
7301
7316
  }
7302
7317
  function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependencies = {}) {
7303
7318
  const stops = /* @__PURE__ */ new Map();
7319
+ let authExpired = false;
7304
7320
  const intervalMilliseconds = Math.max(
7305
7321
  1e3,
7306
7322
  Math.min(3e4, Math.floor(taskLeaseTtlSeconds * 1e3 / 4))
@@ -7320,6 +7336,16 @@ function createChannelLeaseHeartbeatManager(cfg, taskLeaseTtlSeconds, dependenci
7320
7336
  // map does not accumulate stopped leases for the life of the channel.
7321
7337
  onLeaseTerminal: (id) => {
7322
7338
  stops.delete(id);
7339
+ },
7340
+ // A dead credential fails every lease at once, so N held leases would
7341
+ // otherwise raise this N times in the same tick. Latch it: stop every beat
7342
+ // (not just the one that noticed), then escalate exactly once.
7343
+ onAuthExpired: (error) => {
7344
+ if (authExpired) return;
7345
+ authExpired = true;
7346
+ for (const cancel of stops.values()) cancel();
7347
+ stops.clear();
7348
+ dependencies.onAuthExpired?.(error);
7323
7349
  }
7324
7350
  }
7325
7351
  );
@@ -7414,15 +7440,50 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
7414
7440
  await conn.start();
7415
7441
  return conn;
7416
7442
  }
7417
- function holdOpen(conn) {
7443
+ function holdOpen(conn, until) {
7418
7444
  return new Promise((resolve9) => {
7419
7445
  const stop = () => {
7420
7446
  void conn.stop().finally(resolve9);
7421
7447
  };
7422
7448
  process.on("SIGINT", stop);
7423
7449
  process.on("SIGTERM", stop);
7450
+ void until?.then(stop);
7424
7451
  });
7425
7452
  }
7453
+ async function holdChannelOpen(conn, released, teardown) {
7454
+ try {
7455
+ await holdOpen(conn, released);
7456
+ } finally {
7457
+ let firstError;
7458
+ let failed = false;
7459
+ for (const step of teardown) {
7460
+ try {
7461
+ await step();
7462
+ } catch (error) {
7463
+ if (!failed) {
7464
+ failed = true;
7465
+ firstError = error;
7466
+ }
7467
+ }
7468
+ }
7469
+ if (failed) throw firstError;
7470
+ }
7471
+ }
7472
+ function createAuthExpiredExit() {
7473
+ let release;
7474
+ const released = new Promise((resolve9) => {
7475
+ release = resolve9;
7476
+ });
7477
+ return {
7478
+ onAuthExpired: (error) => {
7479
+ process.stderr.write(err(`${authExpiredLine(error)}
7480
+ `));
7481
+ process.exitCode = 1;
7482
+ release();
7483
+ },
7484
+ released
7485
+ };
7486
+ }
7426
7487
  function parseEvent(payload) {
7427
7488
  let data = payload;
7428
7489
  if (typeof payload === "string") {
@@ -7477,92 +7538,6 @@ function str(v) {
7477
7538
  return typeof v === "string" ? v : v == null ? "" : String(v);
7478
7539
  }
7479
7540
 
7480
- // src/commands/chat.ts
7481
- function registerChat(program2) {
7482
- const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
7483
- chat.addHelpText(
7484
- "after",
7485
- `
7486
- Examples:
7487
- $ sechroom chat send C0123456789 "deploy is green" --surface slack
7488
- $ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
7489
- $ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
7490
- $ sechroom chat messages --surface slack
7491
- $ sechroom chat replies 1718049600.123456 --surface slack
7492
- $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
7493
- );
7494
- chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
7495
- const { surface, ...globals } = cmd.optsWithGlobals();
7496
- const json = Boolean(cmd.optsWithGlobals().json);
7497
- const cfg = resolveConfig(globals);
7498
- const data = await runApi("Sending message", async () => {
7499
- const client = await makeClient(cfg);
7500
- return client.POST("/chat/channel-messages/{surface}", {
7501
- params: { path: { surface: String(surface) } },
7502
- body: {
7503
- channelId,
7504
- text: text2,
7505
- guildId: opts.guild ?? null,
7506
- attachedMemoryId: opts.memory ?? null,
7507
- trackReplies: opts.track,
7508
- parentMessage: opts.parent ?? null,
7509
- source: opts.source,
7510
- as: opts.as
7511
- }
7512
- });
7513
- });
7514
- if (!data.ok) {
7515
- if (json) {
7516
- emit(data, true);
7517
- } else {
7518
- process.stderr.write(
7519
- `${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
7520
- `
7521
- );
7522
- }
7523
- process.exit(1);
7524
- }
7525
- const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
7526
- emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
7527
- });
7528
- chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
7529
- const { surface, ...globals } = cmd.optsWithGlobals();
7530
- const cfg = resolveConfig(globals);
7531
- const data = await runApi("Fetching messages", async () => {
7532
- const client = await makeClient(cfg);
7533
- return client.GET("/chat/channel-messages/{surface}", {
7534
- params: { path: { surface: String(surface) } }
7535
- });
7536
- });
7537
- emit(data, cmd.optsWithGlobals().json);
7538
- });
7539
- chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
7540
- const cfg = resolveConfig(cmd.optsWithGlobals());
7541
- const data = await runApi("Fetching replies", async () => {
7542
- const client = await makeClient(cfg);
7543
- return client.GET("/chat/channel-messages/by-id/{id}/replies", {
7544
- params: { path: { id: messageId } }
7545
- });
7546
- });
7547
- emit(data, cmd.optsWithGlobals().json);
7548
- });
7549
- chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
7550
- const cfg = resolveConfig(cmd.optsWithGlobals());
7551
- const data = await runApi("Stopping reply tracking", async () => {
7552
- const client = await makeClient(cfg);
7553
- return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
7554
- params: { path: { id: messageId } },
7555
- body: {}
7556
- });
7557
- });
7558
- emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
7559
- });
7560
- }
7561
-
7562
- // src/commands/checkpoint.ts
7563
- import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
7564
- import { dirname as dirname14, join as join17 } from "path";
7565
-
7566
7541
  // src/commands/hook.ts
7567
7542
  import { createHash as createHash4 } from "crypto";
7568
7543
  import { existsSync as existsSync12, mkdirSync as mkdirSync14, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync13 } from "fs";
@@ -8156,10 +8131,22 @@ function resolveLane(flagLane, cwd) {
8156
8131
  const env = process.env.SECHROOM_LANE;
8157
8132
  if (env) return env;
8158
8133
  const start = cwd ?? process.cwd();
8159
- const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
8134
+ const semPath = resolveSemPathForRead(start);
8135
+ const base = semPath ? readSem(semPath)?.values["code-lane"] : void 0;
8160
8136
  if (!base) return void 0;
8161
8137
  return applyWorktreeLaneSuffix(base, start);
8162
8138
  }
8139
+ function resolveSourceLane(explicit, cwd) {
8140
+ const trimmed = explicit?.trim();
8141
+ if (trimmed) return trimmed;
8142
+ const lane = resolveLane(void 0, cwd);
8143
+ if (!lane) {
8144
+ throw new Error(
8145
+ "no --source and no lane pinned for this checkout \u2014 a record's source must name the lane that wrote it, never a generic value. Pin one with `sechroom lane set --code-lane <id>` (see `sechroom lane`), set SECHROOM_LANE, or pass --source explicitly."
8146
+ );
8147
+ }
8148
+ return lane;
8149
+ }
8163
8150
  var INTENT_FILE = join16(".sechroom", "continuity.json");
8164
8151
  var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
8165
8152
  function resolveIntentPath(start) {
@@ -8196,6 +8183,56 @@ function localDryRunMissingFields(i) {
8196
8183
  ];
8197
8184
  return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
8198
8185
  }
8186
+ var SNAPSHOT_LIMITS = {
8187
+ /** objective / state / lastAction / nextAction / resumeInstruction. */
8188
+ bodyFieldLength: 2e3,
8189
+ /** Any single entry in constraints / questions / surfaceMarkers / artifacts. */
8190
+ listItemLength: 500,
8191
+ /** Entries per list. */
8192
+ listLength: 25
8193
+ };
8194
+ function snapshotLimitViolations(i) {
8195
+ const violations = [];
8196
+ const bodyFields = [
8197
+ "objective",
8198
+ "state",
8199
+ "lastAction",
8200
+ "nextAction",
8201
+ "resumeInstruction"
8202
+ ];
8203
+ for (const key of bodyFields) {
8204
+ const value = i[key];
8205
+ if (typeof value !== "string") continue;
8206
+ if (value.length > SNAPSHOT_LIMITS.bodyFieldLength) {
8207
+ violations.push(
8208
+ `${key}: ${value.length} characters exceeds the ${SNAPSHOT_LIMITS.bodyFieldLength}-character limit`
8209
+ );
8210
+ }
8211
+ }
8212
+ const lists = [
8213
+ "constraints",
8214
+ "questions",
8215
+ "surfaceMarkers",
8216
+ "artifacts"
8217
+ ];
8218
+ for (const key of lists) {
8219
+ const items = i[key];
8220
+ if (!Array.isArray(items)) continue;
8221
+ if (items.length > SNAPSHOT_LIMITS.listLength) {
8222
+ violations.push(
8223
+ `${key}: ${items.length} entries exceeds the ${SNAPSHOT_LIMITS.listLength}-entry limit`
8224
+ );
8225
+ }
8226
+ items.forEach((item, index) => {
8227
+ if (typeof item === "string" && item.length > SNAPSHOT_LIMITS.listItemLength) {
8228
+ violations.push(
8229
+ `${key}[${index}]: ${item.length} characters exceeds the ${SNAPSHOT_LIMITS.listItemLength}-character limit`
8230
+ );
8231
+ }
8232
+ });
8233
+ }
8234
+ return violations;
8235
+ }
8199
8236
  async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
8200
8237
  const lane = resolveLane(laneFlag, cwd);
8201
8238
  if (!lane) return false;
@@ -8510,7 +8547,96 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
8510
8547
  });
8511
8548
  }
8512
8549
 
8550
+ // src/commands/chat.ts
8551
+ function registerChat(program2) {
8552
+ const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
8553
+ chat.addHelpText(
8554
+ "after",
8555
+ `
8556
+ Examples:
8557
+ $ sechroom chat send C0123456789 "deploy is green" --surface slack
8558
+ $ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
8559
+ $ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
8560
+ $ sechroom chat messages --surface slack
8561
+ $ sechroom chat replies 1718049600.123456 --surface slack
8562
+ $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
8563
+ );
8564
+ chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer; default: this checkout's pinned code-lane)").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
8565
+ const source = resolveSourceLane(opts.source);
8566
+ const { surface, ...globals } = cmd.optsWithGlobals();
8567
+ const json = Boolean(cmd.optsWithGlobals().json);
8568
+ const cfg = resolveConfig(globals);
8569
+ const data = await runApi("Sending message", async () => {
8570
+ const client = await makeClient(cfg);
8571
+ return client.POST("/chat/channel-messages/{surface}", {
8572
+ params: { path: { surface: String(surface) } },
8573
+ body: {
8574
+ channelId,
8575
+ text: text2,
8576
+ guildId: opts.guild ?? null,
8577
+ attachedMemoryId: opts.memory ?? null,
8578
+ trackReplies: opts.track,
8579
+ parentMessage: opts.parent ?? null,
8580
+ source,
8581
+ as: opts.as
8582
+ }
8583
+ });
8584
+ });
8585
+ if (!data.ok) {
8586
+ if (json) {
8587
+ emit(data, true);
8588
+ } else {
8589
+ process.stderr.write(
8590
+ `${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
8591
+ `
8592
+ );
8593
+ }
8594
+ process.exit(1);
8595
+ }
8596
+ const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
8597
+ emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
8598
+ });
8599
+ chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
8600
+ const { surface, ...globals } = cmd.optsWithGlobals();
8601
+ const cfg = resolveConfig(globals);
8602
+ const data = await runApi("Fetching messages", async () => {
8603
+ const client = await makeClient(cfg);
8604
+ return client.GET("/chat/channel-messages/{surface}", {
8605
+ params: { path: { surface: String(surface) } }
8606
+ });
8607
+ });
8608
+ emit(data, cmd.optsWithGlobals().json);
8609
+ });
8610
+ chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
8611
+ const cfg = resolveConfig(cmd.optsWithGlobals());
8612
+ const data = await runApi("Fetching replies", async () => {
8613
+ const client = await makeClient(cfg);
8614
+ return client.GET("/chat/channel-messages/by-id/{id}/replies", {
8615
+ params: { path: { id: messageId } }
8616
+ });
8617
+ });
8618
+ emit(data, cmd.optsWithGlobals().json);
8619
+ });
8620
+ chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
8621
+ const cfg = resolveConfig(cmd.optsWithGlobals());
8622
+ const data = await runApi("Stopping reply tracking", async () => {
8623
+ const client = await makeClient(cfg);
8624
+ return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
8625
+ params: { path: { id: messageId } },
8626
+ body: {}
8627
+ });
8628
+ });
8629
+ emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
8630
+ });
8631
+ }
8632
+
8513
8633
  // src/commands/checkpoint.ts
8634
+ import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync14 } from "fs";
8635
+ import { dirname as dirname14, join as join17 } from "path";
8636
+ var CHECKPOINT_UNCHANGED_EXIT_CODE = 3;
8637
+ function checkpointUnchangedLine(snapshotId) {
8638
+ return `${style.bold("=")} unchanged ${style.dim(`(${snapshotId} already current)`)} \u2014 no new snapshot; the local file was left as-is`;
8639
+ }
8514
8640
  function registerCheckpoint(program2) {
8515
8641
  program2.command("checkpoint").description(
8516
8642
  "Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
@@ -8558,6 +8684,13 @@ Examples:
8558
8684
  "no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
8559
8685
  );
8560
8686
  }
8687
+ const overLimit = snapshotLimitViolations(merged);
8688
+ if (overLimit.length > 0) {
8689
+ fail(
8690
+ `snapshot exceeds the field limits \u2014 trim and retry:
8691
+ ${overLimit.join("\n ")}`
8692
+ );
8693
+ }
8561
8694
  const scope = merged.scope ?? "session";
8562
8695
  const body = {
8563
8696
  laneId: lane,
@@ -8604,6 +8737,16 @@ Examples:
8604
8737
  const client = await makeClient(cfg);
8605
8738
  return client.POST("/continuity/snapshots", { body });
8606
8739
  });
8740
+ const previousSnapshotId = base.lastSnapshotId;
8741
+ if (previousSnapshotId && data.snapshotId === previousSnapshotId) {
8742
+ process.exitCode = CHECKPOINT_UNCHANGED_EXIT_CODE;
8743
+ if (json) {
8744
+ emit({ snapshotId: data.snapshotId, lane, scope, unchanged: true, file: null }, true);
8745
+ return;
8746
+ }
8747
+ process.stdout.write(checkpointUnchangedLine(data.snapshotId) + "\n");
8748
+ return;
8749
+ }
8607
8750
  const path = resolveIntentPath(cwd) ?? join17(cwd, INTENT_FILE);
8608
8751
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
8609
8752
  mkdirSync15(dirname14(path), { recursive: true });
@@ -8641,7 +8784,7 @@ function registerClose(program2) {
8641
8784
  ).option(
8642
8785
  "--to-version <n>",
8643
8786
  "Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
8644
- ).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
8787
+ ).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").addHelpText(
8645
8788
  "after",
8646
8789
  `
8647
8790
  Examples:
@@ -8653,6 +8796,7 @@ Examples:
8653
8796
  $ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
8654
8797
  --title "done" --file ./closeout.md`
8655
8798
  ).action(async (opts, cmd) => {
8799
+ const source = resolveSourceLane(opts.source);
8656
8800
  const cfg = resolveConfig(cmd.optsWithGlobals());
8657
8801
  const json = cmd.optsWithGlobals().json;
8658
8802
  const verdict = String(opts.verdict);
@@ -8712,7 +8856,7 @@ Examples:
8712
8856
  type: "reference",
8713
8857
  content: "{}",
8714
8858
  confidence: 1,
8715
- source: opts.source,
8859
+ source,
8716
8860
  archetype: "Document",
8717
8861
  title: opts.title,
8718
8862
  tags,
@@ -8769,7 +8913,7 @@ Examples:
8769
8913
  params: { path: { memoryId: opts.task } },
8770
8914
  body: {
8771
8915
  memoryId: opts.task,
8772
- source: opts.source,
8916
+ source,
8773
8917
  tags: nextTags
8774
8918
  }
8775
8919
  })
@@ -10583,6 +10727,64 @@ AGPL \u2014 no Herdr code is vendored here.`
10583
10727
 
10584
10728
  // src/commands/lane.ts
10585
10729
  var LANE_KEYS = ["code-lane", "design-lane"];
10730
+ function apiFlagsFrom(options) {
10731
+ return {
10732
+ baseUrl: typeof options.baseUrl === "string" ? options.baseUrl : void 0,
10733
+ tenant: typeof options.tenant === "string" ? options.tenant : void 0,
10734
+ account: typeof options.account === "string" ? options.account : void 0,
10735
+ binding: typeof options.binding === "string" ? options.binding : void 0
10736
+ };
10737
+ }
10738
+ function resolvedPinnedLanes(values) {
10739
+ const lanes = LANE_KEYS.flatMap((key) => {
10740
+ const lane = values[key];
10741
+ return lane ? [applyWorktreeLaneSuffix(lane)] : [];
10742
+ });
10743
+ return [...new Set(lanes)];
10744
+ }
10745
+ function reportRegistrationWarning(laneId, error) {
10746
+ const detail = formatFailureMessage(error);
10747
+ const message = `could not register source lane '${laneId}': ${detail}`;
10748
+ process.stderr.write(`${warn(`warning: ${message}`)}
10749
+ `);
10750
+ return message;
10751
+ }
10752
+ async function registerPinnedLanes(laneIds, flags) {
10753
+ if (laneIds.length === 0) return [];
10754
+ let client;
10755
+ try {
10756
+ client = await makeClient(resolveConfig(flags));
10757
+ } catch (error) {
10758
+ return laneIds.map((laneId) => ({
10759
+ laneId,
10760
+ registered: false,
10761
+ warning: reportRegistrationWarning(laneId, error)
10762
+ }));
10763
+ }
10764
+ const results = [];
10765
+ for (const laneId of laneIds) {
10766
+ try {
10767
+ const response = await client.POST("/identity/source-lanes", {
10768
+ body: { laneId }
10769
+ });
10770
+ const failure = response.error ?? (response.response && !response.response.ok ? `HTTP ${response.response.status} ${response.response.statusText}`.trim() : void 0);
10771
+ if (failure !== void 0) throw new Error(formatFailureMessage(failure));
10772
+ results.push({ laneId, registered: true });
10773
+ } catch (error) {
10774
+ results.push({
10775
+ laneId,
10776
+ registered: false,
10777
+ warning: reportRegistrationWarning(laneId, error)
10778
+ });
10779
+ }
10780
+ }
10781
+ return results;
10782
+ }
10783
+ function printRegistrationSummary(registrations) {
10784
+ const registered = registrations.filter((result) => result.registered).map((result) => result.laneId);
10785
+ if (registered.length > 0)
10786
+ console.log(style.dim(`Registered source lane${registered.length === 1 ? "" : "s"}: ${registered.join(", ")}`));
10787
+ }
10586
10788
  function showLane(json) {
10587
10789
  const found = readSem();
10588
10790
  if (!found) {
@@ -10604,35 +10806,57 @@ function showLane(json) {
10604
10806
  Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
10605
10807
  if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
10606
10808
  }
10607
- function setLane(opts) {
10809
+ async function setLane(opts) {
10608
10810
  if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
10609
10811
  const target = localSemPath();
10610
10812
  const values = readLocalSemValues();
10611
10813
  if (opts.codeLane) values["code-lane"] = opts.codeLane;
10612
10814
  if (opts.designLane) values["design-lane"] = opts.designLane;
10613
10815
  writeSem(values, target);
10614
- if (opts.json) return emit({ path: target, values }, true);
10816
+ const registrations = await registerPinnedLanes(
10817
+ resolvedPinnedLanes(values),
10818
+ opts.apiFlags ?? {}
10819
+ );
10820
+ if (opts.json) return emit({ path: target, values, registrations }, true);
10615
10821
  console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
10616
10822
  Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
10823
+ printRegistrationSummary(registrations);
10617
10824
  }
10618
10825
  function registerLane(program2) {
10619
10826
  const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
10620
- lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
10621
- (opts, cmd) => setLane({
10827
+ lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
10828
+ const globals = cmd.optsWithGlobals();
10829
+ return setLane({
10622
10830
  codeLane: opts.codeLane,
10623
10831
  designLane: opts.designLane,
10624
- json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
10625
- })
10626
- );
10832
+ json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json),
10833
+ apiFlags: apiFlagsFrom(globals)
10834
+ });
10835
+ });
10836
+ lane.command("register").description("Register the source lanes already pinned in this checkout").option("--json", "machine output").action(async (opts, cmd) => {
10837
+ const found = readSem();
10838
+ if (!found)
10839
+ fail("No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.");
10840
+ const globals = cmd.optsWithGlobals();
10841
+ const registrations = await registerPinnedLanes(
10842
+ resolvedPinnedLanes(found.values),
10843
+ apiFlagsFrom(globals)
10844
+ );
10845
+ const json = Boolean(opts.json) || Boolean(globals.json);
10846
+ if (json) return emit({ path: found.path, registrations }, true);
10847
+ printRegistrationSummary(registrations);
10848
+ });
10627
10849
  lane.addHelpText(
10628
10850
  "after",
10629
10851
  `
10630
10852
  Examples:
10631
10853
  $ sechroom lane show the resolved lane(s)
10632
10854
  $ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
10855
+ $ sechroom lane register register existing pinned lane(s)
10633
10856
 
10634
10857
  In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
10635
- (Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
10858
+ Lane resolution stops at the nearest git worktree/repository root; a parent checkout's .sechroom/lane.json is never inherited.
10859
+ (Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat; set-lane also registers.)`
10636
10860
  );
10637
10861
  }
10638
10862
 
@@ -10915,7 +11139,8 @@ Examples:
10915
11139
  "--owner-type <ownerType>",
10916
11140
  "Workspace | Project | Unfiled",
10917
11141
  "Unfiled"
10918
- ).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
11142
+ ).option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
11143
+ const source = resolveSourceLane(opts.source);
10919
11144
  const cfg = resolveConfig(cmd.optsWithGlobals());
10920
11145
  const { text: text2, defaultTitle } = resolveCreateBody(opts.text, opts.file);
10921
11146
  const title = opts.title ?? defaultTitle;
@@ -10928,7 +11153,7 @@ Examples:
10928
11153
  type: opts.type,
10929
11154
  content: "{}",
10930
11155
  confidence: Number(opts.confidence),
10931
- source: opts.source,
11156
+ source,
10932
11157
  archetype: "Document",
10933
11158
  title: title ?? null,
10934
11159
  tags: opts.tag ?? null,
@@ -10957,7 +11182,8 @@ Examples:
10957
11182
  "--create-workspace",
10958
11183
  "Create the --workspace when its name matches nothing",
10959
11184
  false
10960
- ).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
11185
+ ).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
11186
+ const source = resolveSourceLane(opts.source);
10961
11187
  const cfg = resolveConfig(cmd.optsWithGlobals());
10962
11188
  const json = Boolean(cmd.optsWithGlobals().json);
10963
11189
  const dryRun = Boolean(opts.dryRun);
@@ -11070,7 +11296,7 @@ ${plan.rows.map(
11070
11296
  workspaceId: workspace.id,
11071
11297
  type: opts.type,
11072
11298
  tags: opts.tag ?? null,
11073
- source: opts.source,
11299
+ source,
11074
11300
  confidence: Number(opts.confidence)
11075
11301
  },
11076
11302
  ports,
@@ -11192,7 +11418,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11192
11418
  "--replace-all",
11193
11419
  "Replace every occurrence (default: first only)",
11194
11420
  false
11195
- ).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11421
+ ).option("--regenerate-filing", "Re-run filing after the edit", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11422
+ const source = resolveSourceLane(opts.source);
11196
11423
  const cfg = resolveConfig(cmd.optsWithGlobals());
11197
11424
  const data = await runApi("Editing memory text", async () => {
11198
11425
  const client = await makeClient(cfg);
@@ -11204,7 +11431,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11204
11431
  newText: opts.new,
11205
11432
  replaceAll: Boolean(opts.replaceAll),
11206
11433
  regenerateFiling: Boolean(opts.regenerateFiling),
11207
- source: opts.source
11434
+ source
11208
11435
  }
11209
11436
  });
11210
11437
  });
@@ -11216,7 +11443,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11216
11443
  });
11217
11444
  memory.command("edit-text-batch <memoryId>").description(
11218
11445
  "Apply many find/replace edits (POST /memories/{memoryId}/edit-text-batch)"
11219
- ).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11446
+ ).requiredOption("--edit <old=>new...>", "Edit as 'old=>new' (repeatable)").option("--replace-all", "Apply replaceAll to every edit", false).option("--regenerate-filing", "Re-run filing after the edits", false).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11447
+ const source = resolveSourceLane(opts.source);
11220
11448
  const cfg = resolveConfig(cmd.optsWithGlobals());
11221
11449
  const replaceAll = Boolean(opts.replaceAll);
11222
11450
  const edits = opts.edit.map((spec) => {
@@ -11242,7 +11470,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11242
11470
  memoryId,
11243
11471
  edits,
11244
11472
  regenerateFiling: Boolean(opts.regenerateFiling),
11245
- source: opts.source
11473
+ source
11246
11474
  }
11247
11475
  });
11248
11476
  });
@@ -11267,7 +11495,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11267
11495
  "--bump-version",
11268
11496
  "Bump the version chain (use for a content reinterpretation, e.g. a type promotion)",
11269
11497
  false
11270
- ).option("--source <source>", "Contributing lane stamp (attribution)", "cli").action(async (memoryId, opts, cmd) => {
11498
+ ).option("--source <source>", "Contributing lane stamp (attribution; default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11499
+ const source = resolveSourceLane(opts.source);
11271
11500
  const cfg = resolveConfig(cmd.optsWithGlobals());
11272
11501
  const json = cmd.optsWithGlobals().json;
11273
11502
  const hasTagOps = Boolean(opts.tag || opts.addTag || opts.removeTag);
@@ -11299,7 +11528,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11299
11528
  }
11300
11529
  const body = {
11301
11530
  memoryId,
11302
- source: opts.source,
11531
+ source,
11303
11532
  bumpVersion: Boolean(opts.bumpVersion)
11304
11533
  };
11305
11534
  if (opts.title !== void 0) body.title = opts.title;
@@ -11323,13 +11552,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11323
11552
  json
11324
11553
  );
11325
11554
  });
11326
- memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11555
+ memory.command("archive <memoryId>").description("Archive a memory (POST /memories/{memoryId}/archive)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11556
+ const source = resolveSourceLane(opts.source);
11327
11557
  const cfg = resolveConfig(cmd.optsWithGlobals());
11328
11558
  const data = await runApi("Archiving memory", async () => {
11329
11559
  const client = await makeClient(cfg);
11330
11560
  return client.POST("/memories/{memoryId}/archive", {
11331
11561
  params: { path: { memoryId } },
11332
- body: { source: opts.source }
11562
+ body: { source }
11333
11563
  });
11334
11564
  });
11335
11565
  emitAction(
@@ -11340,13 +11570,14 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11340
11570
  });
11341
11571
  memory.command("restore <memoryId>").description(
11342
11572
  "Restore an archived memory (POST /memories/{memoryId}/restore)"
11343
- ).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11573
+ ).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11574
+ const source = resolveSourceLane(opts.source);
11344
11575
  const cfg = resolveConfig(cmd.optsWithGlobals());
11345
11576
  const data = await runApi("Restoring memory", async () => {
11346
11577
  const client = await makeClient(cfg);
11347
11578
  return client.POST("/memories/{memoryId}/restore", {
11348
11579
  params: { path: { memoryId } },
11349
- body: { source: opts.source }
11580
+ body: { source }
11350
11581
  });
11351
11582
  });
11352
11583
  emitAction(
@@ -11360,7 +11591,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11360
11591
  ).requiredOption(
11361
11592
  "--owner-type <ownerType>",
11362
11593
  "Unfiled | Workspace | Project | Candidate"
11363
- ).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11594
+ ).option("--owner-id <ownerId>", "Owner id (required unless Unfiled)").option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11595
+ const source = resolveSourceLane(opts.source);
11364
11596
  const cfg = resolveConfig(cmd.optsWithGlobals());
11365
11597
  const data = await runApi("Moving memory", async () => {
11366
11598
  const client = await makeClient(cfg);
@@ -11371,7 +11603,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11371
11603
  type: opts.ownerType,
11372
11604
  id: String(opts.ownerId ?? "")
11373
11605
  },
11374
- source: opts.source
11606
+ source
11375
11607
  }
11376
11608
  });
11377
11609
  });
@@ -11423,7 +11655,8 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11423
11655
  ).requiredOption(
11424
11656
  "--content <content>",
11425
11657
  "Reverted content JSON (the target version's content)"
11426
- ).option("--source <source>", "Source / lane stamp", "cli").action(async (memoryId, opts, cmd) => {
11658
+ ).option("--source <source>", "Source / lane stamp (default: this checkout's pinned code-lane)").action(async (memoryId, opts, cmd) => {
11659
+ const source = resolveSourceLane(opts.source);
11427
11660
  const cfg = resolveConfig(cmd.optsWithGlobals());
11428
11661
  const data = await runApi("Reverting memory", async () => {
11429
11662
  const client = await makeClient(cfg);
@@ -11433,7 +11666,7 @@ Note: CLI memory get omits relationships by default; MCP get_memory includes the
11433
11666
  fromVersion: Number(opts.fromVersion),
11434
11667
  revertedContent: opts.content,
11435
11668
  revertedText: opts.text,
11436
- source: opts.source
11669
+ source
11437
11670
  }
11438
11671
  });
11439
11672
  });
@@ -12445,6 +12678,7 @@ function copyChoice(opts) {
12445
12678
  }
12446
12679
  async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, choice) {
12447
12680
  if (!personalWorkspaceId || choice === "no") return;
12681
+ let source;
12448
12682
  const seen = /* @__PURE__ */ new Set();
12449
12683
  for (const key of keys) {
12450
12684
  const instr = targets[key]?.instruction;
@@ -12473,7 +12707,8 @@ version, the shared template stays clean, and you can discard back anytime.
12473
12707
  make = await promptYesNo("Make a personal copy to customise?");
12474
12708
  }
12475
12709
  if (make) {
12476
- await createOverride(cfg, resolved, personalWorkspaceId);
12710
+ const copySource = source ??= resolveSourceLane(void 0);
12711
+ await createOverride(cfg, resolved, personalWorkspaceId, copySource);
12477
12712
  process.stderr.write(
12478
12713
  `\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
12479
12714
  `
@@ -12837,7 +13072,10 @@ function registerSetup(program2, deps = {}) {
12837
13072
  ).option(
12838
13073
  "--body <markdown>",
12839
13074
  "section body (default: a TODO scaffold to edit later)"
12840
- ).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
13075
+ ).option("--no-regen", "skip the agent-files regen after authoring").option(
13076
+ "--source <source>",
13077
+ "Source / lane stamp (default: this checkout's pinned code-lane)"
13078
+ ).option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
12841
13079
  "after",
12842
13080
  `
12843
13081
  The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
@@ -12850,6 +13088,7 @@ Examples:
12850
13088
  $ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
12851
13089
  $ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
12852
13090
  ).action(async (titleParts, opts, cmd) => {
13091
+ const source = resolveSourceLane(opts.source);
12853
13092
  const cfg = resolveConfig(cmd.optsWithGlobals());
12854
13093
  const json = Boolean(cmd.optsWithGlobals().json);
12855
13094
  const title = titleParts.join(" ").trim();
@@ -12885,7 +13124,7 @@ Examples:
12885
13124
  type: draft.kind,
12886
13125
  content: "{}",
12887
13126
  confidence: 1,
12888
- source: "cli-new-convention",
13127
+ source,
12889
13128
  archetype: "Document",
12890
13129
  title: draft.title,
12891
13130
  tags: draft.tags,
@@ -13463,11 +13702,20 @@ async function ensureTenant(baseUrl, g, opts) {
13463
13702
  clientId: persisted.clientId
13464
13703
  };
13465
13704
  }
13466
- async function ensureAuth(cfg, yes) {
13467
- if (process.env.SECHROOM_TOKEN) return;
13705
+ function hasUsableCredential() {
13706
+ if (process.env.SECHROOM_TOKEN) return true;
13468
13707
  const cached = readToken();
13469
- const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
13470
- if (usable) return;
13708
+ return Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
13709
+ }
13710
+ function requireCredentialForPreview(preview, dryRun) {
13711
+ if (!preview || hasUsableCredential()) return;
13712
+ const flag = dryRun ? "--dry-run" : "--check";
13713
+ fail(
13714
+ `sechroom onboard ${flag} needs an existing sign-in; run \`sechroom login\` first, then re-run the preview.`
13715
+ );
13716
+ }
13717
+ async function ensureAuth(cfg, yes) {
13718
+ if (hasUsableCredential()) return;
13471
13719
  if (!canPrompt() || yes) {
13472
13720
  fail(
13473
13721
  "Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
@@ -13715,6 +13963,10 @@ async function runRecurse(cfg, g, opts) {
13715
13963
  }
13716
13964
  summarizeFanout(results, { dryRun });
13717
13965
  }
13966
+ async function runOnboardCopyPhase(opts) {
13967
+ if (!opts.json && !opts.dryRun && !opts.check) await opts.ensureLanePin();
13968
+ if (!opts.dryRun && !opts.check) await opts.maybeOfferCopies();
13969
+ }
13718
13970
  function registerOnboard(program2) {
13719
13971
  program2.command("onboard").description(
13720
13972
  "Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
@@ -13806,6 +14058,7 @@ Examples:
13806
14058
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
13807
14059
  if (opts.recurse) {
13808
14060
  const baseUrl2 = resolveBaseUrl(g);
14061
+ requireCredentialForPreview(dryRun || check, dryRun);
13809
14062
  await ensureAuth(
13810
14063
  {
13811
14064
  baseUrl: baseUrl2,
@@ -13830,6 +14083,7 @@ Examples:
13830
14083
  return;
13831
14084
  }
13832
14085
  const baseUrl = resolveBaseUrl(g);
14086
+ requireCredentialForPreview(dryRun || check, dryRun);
13833
14087
  await ensureAuth(
13834
14088
  {
13835
14089
  baseUrl,
@@ -13846,7 +14100,12 @@ Examples:
13846
14100
  local: Boolean(opts.local) || scope === "project",
13847
14101
  here: scope === "project" ? true : Boolean(opts.here),
13848
14102
  workspace: opts.workspace,
13849
- persist: !check
14103
+ // Both previews are read-only. `--dry-run` promises to "walk through without
14104
+ // writing files or changing the profile", but only `--check` was in this
14105
+ // guard, so a dry run could still create or rewrite `.sechroom.json` /
14106
+ // `~/.config/sechroom/config.json` before the operator approved anything
14107
+ // (FR-sechroom-710). Matches how `ensureTimezone` is already gated below.
14108
+ persist: !(dryRun || check)
13850
14109
  });
13851
14110
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
13852
14111
  if (!json && tz.action !== "already-set") {
@@ -13925,16 +14184,20 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
13925
14184
  codexScope: scope
13926
14185
  });
13927
14186
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
13928
- if (!dryRun && !check) {
13929
- await maybeOfferCopies(
14187
+ await runOnboardCopyPhase({
14188
+ json,
14189
+ dryRun,
14190
+ check,
14191
+ ensureLanePin: () => ensureLanePin(cfg, { yes, dryRun, clients: keys }),
14192
+ maybeOfferCopies: () => maybeOfferCopies(
13930
14193
  cfg,
13931
14194
  setup,
13932
14195
  targets,
13933
14196
  keys,
13934
14197
  personalWorkspaceId,
13935
14198
  copyChoice(opts)
13936
- );
13937
- }
14199
+ )
14200
+ });
13938
14201
  const writeMcp = wire === "full";
13939
14202
  const result = [];
13940
14203
  for (const key of keys) {
@@ -13957,9 +14220,6 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
13957
14220
  });
13958
14221
  }
13959
14222
  const evalCounts = buildCheckReport(result).eval;
13960
- if (!json && !dryRun) {
13961
- await ensureLanePin(cfg, { yes, dryRun, clients: keys });
13962
- }
13963
14223
  if (!json && !dryRun) {
13964
14224
  for (const t of claudeTargets) {
13965
14225
  await maybeOfferSkills(cfg, personalWorkspaceId, {
@@ -14698,13 +14958,20 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
14698
14958
  if (json) return emit(result, true);
14699
14959
  console.log(style.green("\u2713") + ` wrote ${style.bold(result.path)} ${style.dim(`(${result.bytes} bytes)`)}`);
14700
14960
  });
14701
- skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
14702
- (opts, cmd) => setLane({
14961
+ skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
14962
+ const globals = cmd.optsWithGlobals();
14963
+ return setLane({
14703
14964
  codeLane: opts.codeLane,
14704
14965
  designLane: opts.designLane,
14705
- json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
14706
- })
14707
- );
14966
+ json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json),
14967
+ apiFlags: {
14968
+ baseUrl: globals.baseUrl,
14969
+ tenant: globals.tenant,
14970
+ account: globals.account,
14971
+ binding: globals.binding
14972
+ }
14973
+ });
14974
+ });
14708
14975
  skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
14709
14976
  skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
14710
14977
  if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
@@ -14877,14 +15144,15 @@ Examples:
14877
15144
  $ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
14878
15145
  $ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
14879
15146
  );
14880
- worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId", "cli").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
15147
+ worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId (default: this checkout's pinned code-lane)").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
15148
+ const source = resolveSourceLane(opts.source);
14881
15149
  const cfg = resolveConfig(cmd.optsWithGlobals());
14882
15150
  const data = await runApi("Appending work-log entry", async () => {
14883
15151
  const client = await makeClient(cfg);
14884
15152
  return client.POST("/operator-surface/work-log/append", {
14885
15153
  body: {
14886
15154
  bullet: opts.text,
14887
- laneId: opts.source ?? null,
15155
+ laneId: source,
14888
15156
  workspaceId: opts.workspace ?? null,
14889
15157
  title: opts.title ?? null
14890
15158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.9.1",
3
+ "version": "2026.9.3",
4
4
  "description": "Command-line interface for Sechroom — sign in, wire your AI tools, and work with your sechroom from the terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -43,7 +43,7 @@
43
43
  "gen": "openapi-typescript \"${SECHROOM_OPENAPI_URL:-https://app.sechroom.ai/api/openapi/v1.json}\" -o src/generated/api.d.ts",
44
44
  "build": "tsup src/index.ts --format esm --target node20 --clean",
45
45
  "dev": "tsx src/index.ts",
46
- "test": "node --import tsx --test \"src/**/*.test.ts\"",
46
+ "test": "node scripts/run-tests.mjs",
47
47
  "check-types": "tsc --noEmit"
48
48
  }
49
49
  }