@sechroom/cli 2026.7.20 → 2026.7.21-rc.59d9df67

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 +171 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2213,7 +2213,7 @@ function registerExecutor(program2) {
2213
2213
  );
2214
2214
  });
2215
2215
  executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
2216
- const data = await refresh(
2216
+ const data = await refreshExecutorInstance(
2217
2217
  resolveConfig(cmd.optsWithGlobals()),
2218
2218
  id,
2219
2219
  opts.ttl
@@ -2224,13 +2224,13 @@ function registerExecutor(program2) {
2224
2224
  if (opts.interval >= opts.ttl)
2225
2225
  fail("heartbeat interval must be shorter than the TTL");
2226
2226
  const cfg = resolveConfig(cmd.optsWithGlobals());
2227
- await refresh(cfg, id, opts.ttl);
2227
+ await refreshExecutorInstance(cfg, id, opts.ttl);
2228
2228
  process.stderr.write(
2229
2229
  style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
2230
2230
  `)
2231
2231
  );
2232
2232
  await holdHeartbeat(async () => {
2233
- await refresh(cfg, id, opts.ttl);
2233
+ await refreshExecutorInstance(cfg, id, opts.ttl);
2234
2234
  }, opts.interval * 1e3);
2235
2235
  });
2236
2236
  executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
@@ -2275,7 +2275,7 @@ function parseTransport(value) {
2275
2275
  return fail("transport must be push or pull");
2276
2276
  }
2277
2277
  }
2278
- async function refresh(cfg, id, ttlSeconds) {
2278
+ async function refreshExecutorInstance(cfg, id, ttlSeconds) {
2279
2279
  return api(
2280
2280
  cfg,
2281
2281
  `/me/executor-instances/${encodeURIComponent(id)}/refresh`,
@@ -2367,8 +2367,7 @@ function registerChannel(program2) {
2367
2367
  );
2368
2368
  const withFilterOpts = (c) => c.option(
2369
2369
  "--name <name>",
2370
- "Subscription name (idempotent per name)",
2371
- "wlp-dispatch"
2370
+ "Deprecated: ignored; the installed executor selects its delivery subscription"
2372
2371
  ).option(
2373
2372
  "--tag <tag...>",
2374
2373
  "Deprecated: executor eligibility comes from the installed capability advertisement"
@@ -2404,6 +2403,11 @@ function registerChannel(program2) {
2404
2403
  instance.id
2405
2404
  );
2406
2405
  await drain();
2406
+ const stopReconciliation = startOfferReconciliation(drain);
2407
+ const stopHeartbeat = startExecutorHeartbeat(
2408
+ () => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
2409
+ located.state.refreshAfterSeconds * 1e3
2410
+ );
2407
2411
  if (json) {
2408
2412
  emit(
2409
2413
  {
@@ -2423,7 +2427,12 @@ function registerChannel(program2) {
2423
2427
  ) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
2424
2428
  );
2425
2429
  }
2426
- await holdOpen(conn);
2430
+ try {
2431
+ await holdOpen(conn);
2432
+ } finally {
2433
+ stopReconciliation();
2434
+ stopHeartbeat();
2435
+ }
2427
2436
  });
2428
2437
  withFilterOpts(
2429
2438
  channel.command("mcp").description(
@@ -2464,13 +2473,23 @@ function registerChannel(program2) {
2464
2473
  instance.id
2465
2474
  );
2466
2475
  await drain();
2476
+ const stopReconciliation = startOfferReconciliation(drain);
2477
+ const stopHeartbeat = startExecutorHeartbeat(
2478
+ () => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
2479
+ located.state.refreshAfterSeconds * 1e3
2480
+ );
2467
2481
  process.stderr.write(
2468
2482
  style.dim(
2469
2483
  `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
2470
2484
  `
2471
2485
  )
2472
2486
  );
2473
- await holdOpen(conn);
2487
+ try {
2488
+ await holdOpen(conn);
2489
+ } finally {
2490
+ stopReconciliation();
2491
+ stopHeartbeat();
2492
+ }
2474
2493
  });
2475
2494
  channel.command("install").description(
2476
2495
  "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
@@ -2487,7 +2506,7 @@ function registerChannel(program2) {
2487
2506
  ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
2488
2507
  const path = join9(process.cwd(), ".mcp.json");
2489
2508
  const dryRun = Boolean(opts.dryRun);
2490
- const args = ["channel", "mcp", "--name", opts.name];
2509
+ const args = ["channel", "mcp"];
2491
2510
  const entry = { command: "sechroom", args };
2492
2511
  const config2 = readMcpConfig(path);
2493
2512
  config2.mcpServers ??= {};
@@ -2544,11 +2563,11 @@ function requireExecutorState() {
2544
2563
  return located;
2545
2564
  }
2546
2565
  function warnLegacyChannelOptions(opts) {
2547
- if ((opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
2566
+ if (!opts.name && (opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
2548
2567
  return;
2549
2568
  process.stderr.write(
2550
2569
  style.dim(
2551
- "channel: --workspace, --tag, and --executor-instance are retired; eligibility and identity come from the installed executor advertisement.\n"
2570
+ "channel: --name, --workspace, --tag, and --executor-instance are retired; delivery, eligibility, and identity come from the installed executor advertisement.\n"
2552
2571
  )
2553
2572
  );
2554
2573
  }
@@ -2565,6 +2584,26 @@ function createClaimDrain(cfg, executorInstanceId, deliver, dependencies = {}) {
2565
2584
  return active2;
2566
2585
  };
2567
2586
  }
2587
+ function startOfferReconciliation(drain, intervalMilliseconds = 5e3, dependencies = {}) {
2588
+ const schedule = dependencies.setInterval ?? setInterval;
2589
+ const cancel = dependencies.clearInterval ?? clearInterval;
2590
+ const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel claim failed: ${String(error)}
2591
+ `)));
2592
+ const timer = schedule(() => {
2593
+ void drain().catch(onError);
2594
+ }, intervalMilliseconds);
2595
+ return () => cancel(timer);
2596
+ }
2597
+ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}) {
2598
+ const schedule = dependencies.setInterval ?? setInterval;
2599
+ const cancel = dependencies.clearInterval ?? clearInterval;
2600
+ const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel heartbeat failed: ${String(error)}
2601
+ `)));
2602
+ const timer = schedule(() => {
2603
+ void refresh().catch(onError);
2604
+ }, intervalMilliseconds);
2605
+ return () => cancel(timer);
2606
+ }
2568
2607
  async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
2569
2608
  const request = dependencies.request ?? api;
2570
2609
  const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
@@ -3518,6 +3557,7 @@ Examples:
3518
3557
  }
3519
3558
 
3520
3559
  // src/commands/decomposition.ts
3560
+ import { readFile } from "fs/promises";
3521
3561
  function registerDecomposition(program2) {
3522
3562
  const decomposition = program2.command("decomposition").description(
3523
3563
  "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
@@ -3527,6 +3567,8 @@ function registerDecomposition(program2) {
3527
3567
  `
3528
3568
  Examples:
3529
3569
  $ sechroom decomposition decompose mem_XXXX
3570
+ $ sechroom decomposition from-plan mem_XXXX --file plan.json
3571
+ $ sechroom decomposition plan wlp_XXXX
3530
3572
  $ sechroom decomposition execute sug_XXXX
3531
3573
  $ sechroom decomposition publish-run sug_XXXX
3532
3574
  $ sechroom decomposition accept sug_XXXX
@@ -3549,6 +3591,47 @@ Examples:
3549
3591
  cmd.optsWithGlobals().json
3550
3592
  );
3551
3593
  });
3594
+ decomposition.command("from-plan <briefId>").description(
3595
+ "Create a decomposition from a hand-authored JSON plan; tasks may carry Unix-millisecond wakeAtMs"
3596
+ ).requiredOption(
3597
+ "--file <path>",
3598
+ "JSON file containing { project, tasks, source? }; use - for stdin"
3599
+ ).action(async (briefId, opts, cmd) => {
3600
+ const raw = opts.file === "-" ? await readStdin2() : await readFile(opts.file, "utf8");
3601
+ const body = parsePlanInput(raw, opts.file);
3602
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3603
+ const data = await runApi(
3604
+ "Creating decomposition from plan",
3605
+ async () => {
3606
+ const client = await makeClient(cfg);
3607
+ return client.POST("/work-briefs/{id}/decompose-from-plan", {
3608
+ params: { path: { id: briefId } },
3609
+ body
3610
+ });
3611
+ }
3612
+ );
3613
+ emitAction(
3614
+ `created ${style.bold(data.suggestionId)} from ${data.taskCount} hand-authored task(s)`,
3615
+ data,
3616
+ cmd.optsWithGlobals().json
3617
+ );
3618
+ });
3619
+ decomposition.command("plan <decompositionId>").description(
3620
+ "Inspect the compiled plan, including each scheduled step's Unix-millisecond wakeAtMs"
3621
+ ).action(async (decompositionId, _opts, cmd) => {
3622
+ const cfg = resolveConfig(cmd.optsWithGlobals());
3623
+ const data = await runApi("Reading decomposition plan", async () => {
3624
+ const client = await makeClient(cfg);
3625
+ return client.GET("/decompositions/{id}/plan", {
3626
+ params: { path: { id: decompositionId } }
3627
+ });
3628
+ });
3629
+ emitAction(
3630
+ `read ${style.bold(decompositionId)} plan (${data.steps.length} step(s))`,
3631
+ data,
3632
+ cmd.optsWithGlobals().json
3633
+ );
3634
+ });
3552
3635
  decomposition.command("execute <decompositionId>").description(
3553
3636
  "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
3554
3637
  ).action(async (decompositionId, _opts, cmd) => {
@@ -3618,6 +3701,24 @@ Examples:
3618
3701
  );
3619
3702
  });
3620
3703
  }
3704
+ function parsePlanInput(raw, sourceName = "plan input") {
3705
+ let value;
3706
+ try {
3707
+ value = JSON.parse(raw);
3708
+ } catch (error) {
3709
+ throw new Error(
3710
+ `${sourceName} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
3711
+ );
3712
+ }
3713
+ if (!value || typeof value !== "object" || !Array.isArray(value.tasks))
3714
+ throw new Error(`${sourceName} must contain an object with a tasks array`);
3715
+ return value;
3716
+ }
3717
+ async function readStdin2() {
3718
+ const chunks = [];
3719
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
3720
+ return Buffer.concat(chunks).toString("utf8");
3721
+ }
3621
3722
 
3622
3723
  // src/commands/filing.ts
3623
3724
  function registerFiling(program2) {
@@ -5883,23 +5984,23 @@ Examples:
5883
5984
  });
5884
5985
  emit(data, cmd.optsWithGlobals().json);
5885
5986
  });
5886
- suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{id}/accept)").action(async (id, _opts, cmd) => {
5987
+ suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)").action(async (id, _opts, cmd) => {
5887
5988
  const cfg = resolveConfig(cmd.optsWithGlobals());
5888
5989
  const data = await runApi("Accepting suggestion", async () => {
5889
5990
  const client = await makeClient(cfg);
5890
- return client.POST("/relationship-suggestions/{id}/accept", {
5891
- params: { path: { id } },
5991
+ return client.POST("/relationship-suggestions/{instanceId}/accept", {
5992
+ params: { path: { instanceId: id } },
5892
5993
  body: {}
5893
5994
  });
5894
5995
  });
5895
5996
  emitAction(`accepted suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
5896
5997
  });
5897
- suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{id}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
5998
+ 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) => {
5898
5999
  const cfg = resolveConfig(cmd.optsWithGlobals());
5899
6000
  const data = await runApi("Rejecting suggestion", async () => {
5900
6001
  const client = await makeClient(cfg);
5901
- return client.POST("/relationship-suggestions/{id}/reject", {
5902
- params: { path: { id } },
6002
+ return client.POST("/relationship-suggestions/{instanceId}/reject", {
6003
+ params: { path: { instanceId: id } },
5903
6004
  body: {
5904
6005
  reason: opts.reason ?? null,
5905
6006
  ...opts.reasonCode ? { reasonCode: opts.reasonCode } : {}
@@ -6452,7 +6553,7 @@ function registerTelemetry(program2) {
6452
6553
  "Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
6453
6554
  ).action(async (_opts, cmd) => {
6454
6555
  try {
6455
- const raw = await readStdin2();
6556
+ const raw = await readStdin3();
6456
6557
  const input = parseHookInput2(raw);
6457
6558
  const cwd = input.cwd ?? process.cwd();
6458
6559
  const binding = findBinding(cwd);
@@ -6644,7 +6745,7 @@ function isPermissionNotification(input) {
6644
6745
  if (t) return t.includes("permission");
6645
6746
  return (input.message ?? "").toLowerCase().includes("permission");
6646
6747
  }
6647
- async function readStdin2() {
6748
+ async function readStdin3() {
6648
6749
  if (process.stdin.isTTY) return "";
6649
6750
  const chunks = [];
6650
6751
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -6874,6 +6975,56 @@ Examples:
6874
6975
  });
6875
6976
  }
6876
6977
 
6978
+ // src/commands/work-brief.ts
6979
+ function registerWorkBrief(program2) {
6980
+ const workBrief = program2.command("work-brief").description("Control an active work brief run");
6981
+ workBrief.addHelpText(
6982
+ "after",
6983
+ `
6984
+ Examples:
6985
+ $ sechroom work-brief pause mem_XXXX --reason-code operator-hold --source claude-code-chris
6986
+ $ sechroom work-brief resume mem_XXXX --reason-code operator-resume --source claude-code-chris --reason "Ready to continue"
6987
+ $ sechroom work-brief cancel mem_XXXX --reason-code operator-stopped --source claude-code-chris --reason "Work no longer required"`
6988
+ );
6989
+ registerLifecycleAction(workBrief, "pause");
6990
+ registerLifecycleAction(workBrief, "resume");
6991
+ registerLifecycleAction(workBrief, "cancel");
6992
+ }
6993
+ function registerLifecycleAction(workBrief, action) {
6994
+ const presentParticiple = action === "pause" ? "Pausing" : action === "resume" ? "Resuming" : "Cancelling";
6995
+ const pastTense = action === "pause" ? "paused" : action === "resume" ? "resumed" : "cancelled";
6996
+ workBrief.command(`${action} <briefId>`).description(`${capitalize(action)} an active work brief run`).requiredOption(
6997
+ "--reason-code <code>",
6998
+ "Stable machine-readable reason code"
6999
+ ).requiredOption(
7000
+ "--source <source>",
7001
+ "Calling surface or lane recorded in the audit"
7002
+ ).option("--reason <text>", "Optional human-readable reason").action(async (briefId, opts, cmd) => {
7003
+ const globals = cmd.optsWithGlobals();
7004
+ const cfg = resolveConfig(globals);
7005
+ const data = await runApi(`${presentParticiple} work brief`, async () => {
7006
+ const client = await makeClient(cfg);
7007
+ return client.POST("/work-briefs/{id}/lifecycle", {
7008
+ params: { path: { id: briefId } },
7009
+ body: {
7010
+ action,
7011
+ reasonCode: opts.reasonCode,
7012
+ source: opts.source,
7013
+ reasonText: opts.reason ?? null
7014
+ }
7015
+ });
7016
+ });
7017
+ emitAction(
7018
+ `${pastTense} work brief ${style.bold(briefId)} \u2192 ${data.outcome}`,
7019
+ data,
7020
+ globals.json
7021
+ );
7022
+ });
7023
+ }
7024
+ function capitalize(value) {
7025
+ return value.charAt(0).toUpperCase() + value.slice(1);
7026
+ }
7027
+
6877
7028
  // src/index.ts
6878
7029
  function resolveVersion() {
6879
7030
  try {
@@ -7036,6 +7187,7 @@ registerLookup(program);
7036
7187
  registerRelationships(program);
7037
7188
  registerWorkspace(program);
7038
7189
  registerProject(program);
7190
+ registerWorkBrief(program);
7039
7191
  registerDecomposition(program);
7040
7192
  registerExecutor(program);
7041
7193
  registerClose(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.7.20",
3
+ "version": "2026.7.21-rc.59d9df67",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",