@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 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 { list } from "../lib/args.js";
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
- openQuestions: list(flags.question),
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
- openQuestions: list(flags.question),
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.findings.length === 0) {
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 (report.findings.some((f) => f.severity === "error"))
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.
@@ -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
- flags[key] = inlineVal;
28
+ set(key, inlineVal);
15
29
  }
16
30
  else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
17
- flags[key] = argv[++i];
31
+ set(key, argv[++i]);
18
32
  }
19
33
  else {
20
- flags[key] = true;
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
  *
@@ -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> An open question (repeatable)
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: err.message,
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: err.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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },