@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.
- package/dist/index.js +241 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as 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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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