@sechroom/cli 2026.7.9 → 2026.7.10

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 +241 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync12 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -2533,6 +2533,149 @@ Examples:
2533
2533
  });
2534
2534
  }
2535
2535
 
2536
+ // src/commands/close.ts
2537
+ import { readFileSync as readFileSync7 } from "fs";
2538
+ var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
2539
+ function registerClose(program2) {
2540
+ program2.command("close").description(
2541
+ "Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
2542
+ ).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
2543
+ "--workspace <wsp>",
2544
+ "Workspace that owns the closeout memo"
2545
+ ).requiredOption("--title <title>", "Closeout title").option(
2546
+ "--file <path>",
2547
+ "Read the closeout body from a file (default: stdin)"
2548
+ ).option(
2549
+ "--decomposition <id>",
2550
+ "Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
2551
+ ).option(
2552
+ "--no-status-flip",
2553
+ "Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
2554
+ ).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
2555
+ "after",
2556
+ `
2557
+ Examples:
2558
+ # Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
2559
+ $ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
2560
+ --verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
2561
+
2562
+ # Bare task:
2563
+ $ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
2564
+ --title "done" --file ./closeout.md`
2565
+ ).action(async (opts, cmd) => {
2566
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2567
+ const json = cmd.optsWithGlobals().json;
2568
+ const verdict = String(opts.verdict);
2569
+ if (!VERDICTS.includes(verdict))
2570
+ fail(
2571
+ `--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
2572
+ );
2573
+ let bodyText;
2574
+ try {
2575
+ bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
2576
+ } catch {
2577
+ fail(
2578
+ opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
2579
+ );
2580
+ }
2581
+ if (!bodyText.trim())
2582
+ fail(
2583
+ "closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
2584
+ );
2585
+ const client = await makeClient(cfg);
2586
+ let matchTags;
2587
+ if (opts.decomposition) {
2588
+ const run = await runApi(
2589
+ "Reading run contract",
2590
+ async () => client.GET("/decompositions/{id}/run", {
2591
+ params: { path: { id: opts.decomposition } }
2592
+ })
2593
+ );
2594
+ const await_ = run.awaitingCompletion;
2595
+ if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
2596
+ fail(
2597
+ `decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
2598
+ );
2599
+ if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
2600
+ fail(
2601
+ `run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
2602
+ );
2603
+ matchTags = await_.matchTags;
2604
+ } else {
2605
+ matchTags = [`wlp-task:${opts.task}`];
2606
+ }
2607
+ const tags = [
2608
+ .../* @__PURE__ */ new Set([
2609
+ ...matchTags,
2610
+ `wlp-task:${opts.task}`,
2611
+ `verdict:${verdict}`
2612
+ ])
2613
+ ];
2614
+ const closeout = await runApi(
2615
+ "Creating closeout",
2616
+ async () => client.POST("/memories", {
2617
+ body: {
2618
+ text: bodyText,
2619
+ type: "reference",
2620
+ content: "{}",
2621
+ confidence: 1,
2622
+ source: opts.source,
2623
+ archetype: "Document",
2624
+ title: opts.title,
2625
+ tags,
2626
+ owner: { type: "Workspace", id: opts.workspace }
2627
+ }
2628
+ })
2629
+ );
2630
+ const edge = await runApi(
2631
+ "Wiring Reference edge",
2632
+ async () => client.POST("/memories/{memoryId}/relationships", {
2633
+ params: { path: { memoryId: closeout.id } },
2634
+ body: { toMemoryId: opts.task, type: "Reference" }
2635
+ })
2636
+ );
2637
+ let taskStatus;
2638
+ if (opts.statusFlip !== false && !opts.decomposition) {
2639
+ const current = await runApi(
2640
+ "Reading task tags",
2641
+ async () => client.GET("/memories/{memoryId}", {
2642
+ params: { path: { memoryId: opts.task } }
2643
+ })
2644
+ );
2645
+ const currentTags = current?.item?.tags ?? current?.tags ?? [];
2646
+ const target = verdict === "blocked" ? "status:blocked" : "status:done";
2647
+ const nextTags = [
2648
+ ...new Set(currentTags.filter((t) => !t.startsWith("status:")))
2649
+ ].concat(target);
2650
+ await runApi(
2651
+ "Flipping task status",
2652
+ async () => client.PATCH("/memories/{memoryId}/metadata", {
2653
+ params: { path: { memoryId: opts.task } },
2654
+ body: {
2655
+ memoryId: opts.task,
2656
+ source: opts.source,
2657
+ tags: nextTags
2658
+ }
2659
+ })
2660
+ );
2661
+ taskStatus = target.slice("status:".length);
2662
+ }
2663
+ const view = resolveViewUrl(cfg.baseUrl, closeout.url);
2664
+ const result = {
2665
+ closeoutId: closeout.id,
2666
+ edgeId: edge.id,
2667
+ taskStatus: taskStatus ?? null,
2668
+ verdict,
2669
+ tags
2670
+ };
2671
+ emitAction(
2672
+ `closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
2673
+ result,
2674
+ json
2675
+ );
2676
+ });
2677
+ }
2678
+
2536
2679
  // src/commands/continuity.ts
2537
2680
  function registerContinuity(program2) {
2538
2681
  const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
@@ -2693,6 +2836,90 @@ Examples:
2693
2836
  });
2694
2837
  }
2695
2838
 
2839
+ // src/commands/decomposition.ts
2840
+ function registerDecomposition(program2) {
2841
+ const decomposition = program2.command("decomposition").description(
2842
+ "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
2843
+ );
2844
+ decomposition.addHelpText(
2845
+ "after",
2846
+ `
2847
+ Examples:
2848
+ $ sechroom decomposition decompose mem_XXXX
2849
+ $ sechroom decomposition execute sug_XXXX
2850
+ $ sechroom decomposition accept sug_XXXX
2851
+ $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
2852
+ );
2853
+ decomposition.command("decompose <briefId>").description(
2854
+ "Decompose a governed brief into a candidate Task graph (POST /governed-briefs/{id}/decompose)"
2855
+ ).action(async (briefId, _opts, cmd) => {
2856
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2857
+ const data = await runApi("Queueing decomposition", async () => {
2858
+ const client = await makeClient(cfg);
2859
+ return client.POST("/governed-briefs/{id}/decompose", {
2860
+ params: { path: { id: briefId } },
2861
+ body: { id: briefId }
2862
+ });
2863
+ });
2864
+ emitAction(
2865
+ `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
2866
+ data,
2867
+ cmd.optsWithGlobals().json
2868
+ );
2869
+ });
2870
+ decomposition.command("execute <decompositionId>").description(
2871
+ "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
2872
+ ).action(async (decompositionId, _opts, cmd) => {
2873
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2874
+ const data = await runApi("Executing decomposition", async () => {
2875
+ const client = await makeClient(cfg);
2876
+ return client.POST("/decompositions/{id}/execute", {
2877
+ params: { path: { id: decompositionId } },
2878
+ body: {}
2879
+ });
2880
+ });
2881
+ emitAction(
2882
+ `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
2883
+ data,
2884
+ cmd.optsWithGlobals().json
2885
+ );
2886
+ });
2887
+ decomposition.command("accept <decompositionId>").description(
2888
+ "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
2889
+ ).action(async (decompositionId, _opts, cmd) => {
2890
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2891
+ const data = await runApi("Accepting decomposition", async () => {
2892
+ const client = await makeClient(cfg);
2893
+ return client.POST("/decompositions/{id}/accept", {
2894
+ params: { path: { id: decompositionId } },
2895
+ body: {}
2896
+ });
2897
+ });
2898
+ emitAction(
2899
+ `accepted decomposition ${style.bold(decompositionId)}`,
2900
+ data,
2901
+ cmd.optsWithGlobals().json
2902
+ );
2903
+ });
2904
+ decomposition.command("reject <decompositionId>").description(
2905
+ "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
2906
+ ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
2907
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2908
+ const data = await runApi("Rejecting decomposition", async () => {
2909
+ const client = await makeClient(cfg);
2910
+ return client.POST("/decompositions/{id}/reject", {
2911
+ params: { path: { id: decompositionId } },
2912
+ body: { reasonText: opts.reason ?? null }
2913
+ });
2914
+ });
2915
+ emitAction(
2916
+ `rejected decomposition ${style.bold(decompositionId)}`,
2917
+ data,
2918
+ cmd.optsWithGlobals().json
2919
+ );
2920
+ });
2921
+ }
2922
+
2696
2923
  // src/commands/filing.ts
2697
2924
  function registerFiling(program2) {
2698
2925
  const filing = program2.command("filing").description("Review and act on filing suggestions");
@@ -3204,7 +3431,7 @@ Examples:
3204
3431
 
3205
3432
  // src/setup/apply.ts
3206
3433
  import { createHash as createHash3 } from "crypto";
3207
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3434
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3208
3435
  import { dirname as dirname8 } from "path";
3209
3436
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3210
3437
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -3262,7 +3489,7 @@ function ensureDir2(path) {
3262
3489
  }
3263
3490
  function readOr(path, fallback) {
3264
3491
  try {
3265
- return readFileSync7(path, "utf8");
3492
+ return readFileSync8(path, "utf8");
3266
3493
  } catch {
3267
3494
  return fallback;
3268
3495
  }
@@ -3273,7 +3500,7 @@ function mergeMcpJson(path, snippet, dryRun) {
3273
3500
  let current = {};
3274
3501
  if (existed) {
3275
3502
  try {
3276
- current = JSON.parse(readFileSync7(path, "utf8"));
3503
+ current = JSON.parse(readFileSync8(path, "utf8"));
3277
3504
  } catch {
3278
3505
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3279
3506
  }
@@ -3989,7 +4216,7 @@ import { basename as basename2, join as join13 } from "path";
3989
4216
 
3990
4217
  // src/commands/fanout.ts
3991
4218
  import { spawnSync } from "child_process";
3992
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
4219
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3993
4220
  import { isAbsolute, join as join12, resolve } from "path";
3994
4221
  var ICON = {
3995
4222
  refresh: "\u21BB",
@@ -4024,7 +4251,7 @@ function readManifest(path) {
4024
4251
  if (!existsSync10(path)) return null;
4025
4252
  let parsed;
4026
4253
  try {
4027
- parsed = JSON.parse(readFileSync8(path, "utf8"));
4254
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
4028
4255
  } catch (err2) {
4029
4256
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
4030
4257
  }
@@ -4979,7 +5206,7 @@ Examples:
4979
5206
  // src/commands/reset.ts
4980
5207
  import { homedir as homedir4 } from "os";
4981
5208
  import { join as join14 } from "path";
4982
- import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
5209
+ import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
4983
5210
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4984
5211
  var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
4985
5212
  var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
@@ -4990,7 +5217,7 @@ function removeMaterialisedSkills(dir) {
4990
5217
  const lockPath = join14(dir, SKILLS_LOCK2);
4991
5218
  if (!existsSync12(lockPath)) return removed;
4992
5219
  try {
4993
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
5220
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
4994
5221
  for (const entry of Object.values(lock)) {
4995
5222
  for (const name of entry.skills ?? []) {
4996
5223
  const p = join14(dir, name);
@@ -5390,7 +5617,7 @@ Examples:
5390
5617
  import {
5391
5618
  existsSync as existsSync15,
5392
5619
  mkdirSync as mkdirSync12,
5393
- readFileSync as readFileSync10,
5620
+ readFileSync as readFileSync11,
5394
5621
  rmSync as rmSync4,
5395
5622
  writeFileSync as writeFileSync12
5396
5623
  } from "fs";
@@ -5605,7 +5832,7 @@ function findBinding(start) {
5605
5832
  if (existsSync15(path)) {
5606
5833
  try {
5607
5834
  const b = JSON.parse(
5608
- readFileSync10(path, "utf8")
5835
+ readFileSync11(path, "utf8")
5609
5836
  );
5610
5837
  if (b.decompositionId && b.taskId)
5611
5838
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5624,7 +5851,7 @@ function parseTranscript(path) {
5624
5851
  let tokensOut = 0;
5625
5852
  let contextUsed = 0;
5626
5853
  let model = "";
5627
- for (const line of readFileSync10(path, "utf8").split("\n")) {
5854
+ for (const line of readFileSync11(path, "utf8").split("\n")) {
5628
5855
  if (!line.trim()) continue;
5629
5856
  let obj;
5630
5857
  try {
@@ -5882,7 +6109,7 @@ Examples:
5882
6109
  function resolveVersion() {
5883
6110
  try {
5884
6111
  const pkg = JSON.parse(
5885
- readFileSync11(new URL("../package.json", import.meta.url), "utf8")
6112
+ readFileSync12(new URL("../package.json", import.meta.url), "utf8")
5886
6113
  );
5887
6114
  return pkg.version ?? "0.0.0";
5888
6115
  } catch {
@@ -6040,6 +6267,8 @@ registerLookup(program);
6040
6267
  registerRelationships(program);
6041
6268
  registerWorkspace(program);
6042
6269
  registerProject(program);
6270
+ registerDecomposition(program);
6271
+ registerClose(program);
6043
6272
  registerFiling(program);
6044
6273
  registerContinuity(program);
6045
6274
  registerCheckpoint(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.7.9",
3
+ "version": "2026.7.10",
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",