@esneiderbravo/speclaw 2.0.11 → 2.0.12
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/README.md +4 -0
- package/dist/cli/commands/cortex.js +3 -2
- package/dist/cli/commands/lawbook.js +3 -2
- package/dist/cli/commands/laws.js +10 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/lib/args.js +35 -5
- package/dist/cli/lib/help.js +1 -1
- package/dist/modules/foundation/doctor.js +1 -1
- package/dist/modules/foundation/integrity.js +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,6 +310,10 @@ speclaw laws accept AGENTS.md # interactive TTY only — never via MCP
|
|
|
310
310
|
speclaw laws lock --force # list drifted files, confirm, re-baseline — interactive TTY only
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
`speclaw laws scan` (text or `--json`) exits 1 when `speclaw.lock` cannot be
|
|
314
|
+
read or a finding has severity `error`. On an unreadable lock it names the lock
|
|
315
|
+
error (`lockError` in the JSON) and still reports every injection finding.
|
|
316
|
+
|
|
313
317
|
A lock refresh (`init`, `update`, `laws compile`, `laws lock`) never launders an
|
|
314
318
|
edit: a strict file (`CLAUDE.md`, `AGENTS.md`, compiled rules) that drifted from
|
|
315
319
|
the lock outside speclaw **keeps its locked digest**, and the command warns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { handleHarness, readHarness } from "../../modules/cortex/harness.js";
|
|
2
2
|
import { briefForStage } from "../../modules/cortex/brief.js";
|
|
3
|
-
import {
|
|
3
|
+
import { repeated } from "../lib/args.js";
|
|
4
4
|
import { ui } from "../lib/ui.js";
|
|
5
5
|
const OPS = ["status", "start", "advance", "rework", "brief"];
|
|
6
6
|
/**
|
|
@@ -36,7 +36,8 @@ export async function runCortex(flags) {
|
|
|
36
36
|
change: changeName,
|
|
37
37
|
harnessOp: op,
|
|
38
38
|
verdict: verdict ?? null,
|
|
39
|
-
|
|
39
|
+
// Covers: req~harness-state~1
|
|
40
|
+
openQuestions: repeated(flags.question),
|
|
40
41
|
pauseForQuestions: Boolean(flags["pause-questions"]),
|
|
41
42
|
note: typeof flags.note === "string" ? flags.note : undefined,
|
|
42
43
|
});
|
|
@@ -5,7 +5,7 @@ import { scaffoldFeature } from "../../modules/lawbook/scaffold-change.js";
|
|
|
5
5
|
import { investigate, formatInvestigateResult } from "../../modules/lawbook/investigate.js";
|
|
6
6
|
import { handleHarness } from "../../modules/cortex/harness.js";
|
|
7
7
|
import { printHarnessWarnings } from "./cortex.js";
|
|
8
|
-
import { list } from "../lib/args.js";
|
|
8
|
+
import { list, repeated } from "../lib/args.js";
|
|
9
9
|
import { ui } from "../lib/ui.js";
|
|
10
10
|
function today() {
|
|
11
11
|
// The MCP path passes the date in; the CLL runs on a real machine, so read it here.
|
|
@@ -181,7 +181,8 @@ export async function runSpec(flags) {
|
|
|
181
181
|
change: changeName,
|
|
182
182
|
harnessOp: op,
|
|
183
183
|
verdict: verdict ?? null,
|
|
184
|
-
|
|
184
|
+
// Covers: req~harness-state~1
|
|
185
|
+
openQuestions: repeated(flags.question),
|
|
185
186
|
pauseForQuestions: Boolean(flags["pause-questions"]),
|
|
186
187
|
note: typeof flags.note === "string" ? flags.note : undefined,
|
|
187
188
|
});
|
|
@@ -73,13 +73,21 @@ export async function runLaws(flags) {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
if (sub === "scan") {
|
|
76
|
+
// Text and --json share one exit rule: 1 on an unreadable lock or any
|
|
77
|
+
// error-severity finding, else 0.
|
|
78
|
+
// Covers: req~injection-scan~1, req~laws-integrity-cli~1
|
|
76
79
|
const report = verifyIntegrity({ projectPath: process.cwd(), checks: "scan" });
|
|
80
|
+
const failed = report.lockError !== undefined || report.findings.some((f) => f.severity === "error");
|
|
77
81
|
if (flags.json) {
|
|
78
82
|
console.log(JSON.stringify(report, null, 2));
|
|
83
|
+
if (failed)
|
|
84
|
+
process.exit(1);
|
|
79
85
|
return;
|
|
80
86
|
}
|
|
81
87
|
ui.heading("speclaw laws scan");
|
|
82
|
-
if (report.
|
|
88
|
+
if (report.lockError !== undefined)
|
|
89
|
+
ui.err(report.lockError);
|
|
90
|
+
if (report.findings.length === 0 && report.lockError === undefined) {
|
|
83
91
|
ui.ok("No injection findings.");
|
|
84
92
|
return;
|
|
85
93
|
}
|
|
@@ -91,7 +99,7 @@ export async function runLaws(flags) {
|
|
|
91
99
|
else
|
|
92
100
|
ui.warn(msg);
|
|
93
101
|
}
|
|
94
|
-
if (
|
|
102
|
+
if (failed)
|
|
95
103
|
process.exit(1);
|
|
96
104
|
return;
|
|
97
105
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFlags } from "./lib/args.js";
|
|
2
|
+
import { parseFlags, REPEATABLE_FLAGS } from "./lib/args.js";
|
|
3
3
|
import { ui, header } from "./lib/ui.js";
|
|
4
4
|
import { maybeNotifyUpdate } from "./lib/update-check.js";
|
|
5
5
|
import { GLOBAL_HELP as HELP, helpFor, knownCommands, wantsHelp } from "./lib/help.js";
|
|
@@ -166,7 +166,7 @@ async function main() {
|
|
|
166
166
|
process.exitCode = 0;
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
-
const flags = parseFlags(rest);
|
|
169
|
+
const flags = parseFlags(rest, REPEATABLE_FLAGS);
|
|
170
170
|
maybeHeader(cmd, flags);
|
|
171
171
|
await dispatch(cmd, flags);
|
|
172
172
|
// The SessionStart and edit hooks must stay silent: no update notice either.
|
package/dist/cli/lib/args.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal flag parser: `--key value`, `--key=value`, `--bool`, `-x`.
|
|
2
|
+
* Minimal flag parser: `--key value`, `--key=value`, `--bool`, `-x`. A repeated
|
|
3
|
+
* key keeps its last value unless it is listed in `repeatable`.
|
|
3
4
|
*
|
|
4
5
|
* @param argv - Raw argument tokens (already stripped of the command name).
|
|
6
|
+
* @param repeatable - Keys whose every value is kept: such a key always yields a
|
|
7
|
+
* `string[]` in argv order, and its values are never split at commas.
|
|
5
8
|
* @returns Flags with named options set and non-flag tokens collected in `_`.
|
|
6
9
|
*/
|
|
7
|
-
export function parseFlags(argv) {
|
|
10
|
+
export function parseFlags(argv, repeatable = []) {
|
|
8
11
|
const flags = { _: [] };
|
|
12
|
+
const set = (key, value) => {
|
|
13
|
+
const prev = flags[key];
|
|
14
|
+
if (repeatable.includes(key) && (typeof value === "string" || Array.isArray(prev))) {
|
|
15
|
+
// A bare repeatable flag after real values adds nothing and drops nothing.
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
flags[key] = Array.isArray(prev) ? [...prev, value] : [value];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
flags[key] = value;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
9
23
|
for (let i = 0; i < argv.length; i++) {
|
|
10
24
|
const arg = argv[i];
|
|
11
25
|
if (arg.startsWith("--")) {
|
|
12
26
|
const [key, inlineVal] = arg.slice(2).split("=", 2);
|
|
13
27
|
if (inlineVal !== undefined) {
|
|
14
|
-
|
|
28
|
+
set(key, inlineVal);
|
|
15
29
|
}
|
|
16
30
|
else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
17
|
-
|
|
31
|
+
set(key, argv[++i]);
|
|
18
32
|
}
|
|
19
33
|
else {
|
|
20
|
-
|
|
34
|
+
set(key, true);
|
|
21
35
|
}
|
|
22
36
|
}
|
|
23
37
|
else if (arg.startsWith("-")) {
|
|
@@ -29,6 +43,22 @@ export function parseFlags(argv) {
|
|
|
29
43
|
}
|
|
30
44
|
return flags;
|
|
31
45
|
}
|
|
46
|
+
/** Flags that may be passed more than once; each occurrence is one value. */
|
|
47
|
+
export const REPEATABLE_FLAGS = ["question"];
|
|
48
|
+
/**
|
|
49
|
+
* The values of a repeatable flag parsed with {@link REPEATABLE_FLAGS}, as-is:
|
|
50
|
+
* in argv order and never split at commas.
|
|
51
|
+
*
|
|
52
|
+
* @param value - The parsed flag value.
|
|
53
|
+
* @returns The string values; empty when the flag is absent or a bare boolean.
|
|
54
|
+
*/
|
|
55
|
+
export function repeated(value) {
|
|
56
|
+
if (Array.isArray(value))
|
|
57
|
+
return value;
|
|
58
|
+
if (typeof value === "string")
|
|
59
|
+
return [value];
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
32
62
|
/**
|
|
33
63
|
* Normalize a flag value into a list, splitting comma-separated strings.
|
|
34
64
|
*
|
package/dist/cli/lib/help.js
CHANGED
|
@@ -331,7 +331,7 @@ Options
|
|
|
331
331
|
--change <name> The change (or pass it as the second argument)
|
|
332
332
|
--verdict <v> PASS or FAIL (advance from reviewing/testing)
|
|
333
333
|
--note <text> A history note
|
|
334
|
-
--question <text>
|
|
334
|
+
--question <text> One open question, commas included; repeat for more
|
|
335
335
|
--pause-questions Pause the loop for open questions
|
|
336
336
|
--json Print the result as JSON
|
|
337
337
|
`,
|
|
@@ -563,7 +563,7 @@ function specsOrphansCheck(projectPath) {
|
|
|
563
563
|
* accepting any pending drift — so that is offered only as a last resort.
|
|
564
564
|
*/
|
|
565
565
|
export const UNREADABLE_LOCK_REMEDY = "resolve the merge conflict in speclaw.lock or restore it from git " +
|
|
566
|
-
"(git checkout -- speclaw.lock); upgrade speclaw if the lockfileVersion is newer. " +
|
|
566
|
+
"(git checkout HEAD -- speclaw.lock); upgrade speclaw if the lockfileVersion is newer. " +
|
|
567
567
|
"Last resort: delete it and run speclaw laws lock — this re-baselines every " +
|
|
568
568
|
"pinned file and accepts any pending drift";
|
|
569
569
|
function integrityChecks(projectPath) {
|
|
@@ -23,23 +23,31 @@ export function verifyIntegrity(opts) {
|
|
|
23
23
|
lock = readLockfile(projectPath);
|
|
24
24
|
}
|
|
25
25
|
catch (err) {
|
|
26
|
+
// An unreadable lock only blocks the digest/symlink comparison; the scan
|
|
27
|
+
// never needs the lock, so its findings must still be reported.
|
|
28
|
+
// Covers: req~injection-scan~1
|
|
29
|
+
const lockError = err.message;
|
|
30
|
+
const scanned = doScan ? scanAll(projectPath) : [];
|
|
26
31
|
return {
|
|
27
32
|
ok: false,
|
|
28
33
|
lockPresent: fs.existsSync(lockfilePath(projectPath)),
|
|
29
34
|
rootMatches: false,
|
|
30
|
-
guidance:
|
|
35
|
+
guidance: lockError,
|
|
31
36
|
files: [],
|
|
32
37
|
symlinks: [],
|
|
33
|
-
findings:
|
|
38
|
+
findings: scanned,
|
|
34
39
|
verifyFindings: [
|
|
35
40
|
{
|
|
36
41
|
lawId: "integrity~lockfile~1",
|
|
37
42
|
severity: "error",
|
|
38
43
|
engine: "integrity",
|
|
39
44
|
file: "speclaw.lock",
|
|
40
|
-
message:
|
|
45
|
+
message: lockError,
|
|
41
46
|
},
|
|
47
|
+
...scanned.filter((f) => f.severity === "error").map(scanToFinding),
|
|
48
|
+
...scanned.filter((f) => f.severity === "warn").map(scanToFinding),
|
|
42
49
|
],
|
|
50
|
+
lockError,
|
|
43
51
|
};
|
|
44
52
|
}
|
|
45
53
|
if (!lock && doIntegrity) {
|