@descent-vtt/spec-harness 0.13.0 → 0.15.0

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 (52) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/README.md +18 -3
  5. package/dist/answers.d.ts +6 -0
  6. package/dist/answers.js +12 -1
  7. package/dist/answers.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +81 -43
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +1 -0
  12. package/dist/commands.js.map +1 -1
  13. package/dist/configure.d.ts +58 -2
  14. package/dist/configure.js +116 -3
  15. package/dist/configure.js.map +1 -1
  16. package/dist/context.d.ts +16 -7
  17. package/dist/context.js +36 -11
  18. package/dist/context.js.map +1 -1
  19. package/dist/fs.js +1 -0
  20. package/dist/fs.js.map +1 -1
  21. package/dist/git.js +2 -1
  22. package/dist/git.js.map +1 -1
  23. package/dist/hooks.js.map +1 -1
  24. package/dist/host.js.map +1 -1
  25. package/dist/literal.d.ts +18 -0
  26. package/dist/literal.js +21 -0
  27. package/dist/literal.js.map +1 -0
  28. package/dist/probe.js +1 -1
  29. package/dist/probe.js.map +1 -1
  30. package/dist/reader.js +2 -0
  31. package/dist/reader.js.map +1 -1
  32. package/dist/round.d.ts +12 -1
  33. package/dist/round.js +23 -7
  34. package/dist/round.js.map +1 -1
  35. package/dist/sandbox.js.map +1 -1
  36. package/dist/server.js +2 -1
  37. package/dist/server.js.map +1 -1
  38. package/dist/setup.js +2 -1
  39. package/dist/setup.js.map +1 -1
  40. package/dist/siblings.js +2 -1
  41. package/dist/siblings.js.map +1 -1
  42. package/dist/signers.js +5 -0
  43. package/dist/signers.js.map +1 -1
  44. package/dist/usage.d.ts +83 -27
  45. package/dist/usage.js +61 -16
  46. package/dist/usage.js.map +1 -1
  47. package/dist/versions.js +3 -0
  48. package/dist/versions.js.map +1 -1
  49. package/dist/workspace.d.ts +9 -0
  50. package/dist/workspace.js +48 -32
  51. package/dist/workspace.js.map +1 -1
  52. package/package.json +3 -3
@@ -10,7 +10,7 @@
10
10
  "name": "spec-harness",
11
11
  "source": "./",
12
12
  "description": "Rounds of agent work under a brief.",
13
- "version": "0.13.0"
13
+ "version": "0.15.0"
14
14
  }
15
15
  ]
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-harness",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Rounds of agent work under a brief: context, a guard on every write, escalations a person signs, an audit at the end. Needs Claude Code 2.1.139 or later, and @descent-vtt/spec-harness and @descent-vtt/spec-brief 0.2.0 or later installed in the project.",
5
5
  "author": {
6
6
  "name": "DescentVTT",
package/CHANGELOG.md CHANGED
@@ -9,6 +9,81 @@ only fixes, reports less or documents, so `^0.7.0` takes only releases that
9
9
  cannot turn a run red. The family's policy is
10
10
  [spec-core's ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md).
11
11
 
12
+ ## 0.15.0
13
+
14
+ `doctor` reads the line of Claude Code's hook as the hook itself reads it:
15
+ a line with more on it than `init` writes is said to guard where it does,
16
+ and a line the hook cannot read, or whose `--root` is no directory, is said
17
+ as a guard that is off, exit 1, where each was reported as no guard at all
18
+ or not reported.
19
+
20
+ ### Changed
21
+
22
+ - **`doctor` reads the line of Claude Code's hook as the hook reads it.** It
23
+ knew the hook only where its command and arguments were exactly what
24
+ `init` writes, or what 0.1 wrote: a line with `--root
25
+ ${CLAUDE_PROJECT_DIR}` added by hand, which guards, was reported as
26
+ `neither the spec-harness plugin nor the hooks init writes, so Claude Code
27
+ guards no write`, and a line that turns the guard off was reported the
28
+ same way, with exit 0. It now knows the hook by its command and the words
29
+ `hook claude`, whatever else is on the line, and reads the rest through
30
+ the code the hook reads it with. A line that guards with more on it than
31
+ `init` writes is said as it is and fails nothing. A line the hook cannot
32
+ read, and a `--root` that is not a directory, are said in the hook's own
33
+ words, as a guard that is off, and `doctor` exits 1. A line with no
34
+ `args`, which a shell reads first, is *cannot tell* where the shell would
35
+ change what follows the command: said, and a failure under `--strict`.
36
+ `"onFailure": "block"` on the entry is said in a note, with what it
37
+ changes. `--format json` has each line as `claudeCode.lines`
38
+ ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
39
+ Upgrading: a hook's line changed by hand is now the guard to `doctor`, so
40
+ it asks Claude Code its release for it, and beside the plugin it is the
41
+ double install it always was, exit 1. Where `doctor` now exits 1 for a
42
+ line, the guard was off already: mend the line it names, or write
43
+ `hook claude` and nothing after it, as `init` does. The lines `init` and
44
+ the plugin wrote are reported as before.
45
+
46
+ ## 0.14.0
47
+
48
+ `hook claude` says a line of its own that it cannot read with exit 1 and
49
+ holds no write for it, where exit 2 held every write of the session. And
50
+ where spec-guard refuses for a spec pattern that matched no file, the
51
+ context packet lists the rules of the files that did match, under a
52
+ sentence that names the pattern, where it listed none.
53
+
54
+ ### Changed
55
+
56
+ - **`hook claude` says a line it cannot read, and exits 1.** An option no
57
+ command has, an option left without its value, and a `--format` or an
58
+ `--at` that is none were exit 2 from Claude Code's hook as from every
59
+ command, and Claude Code holds a write on a PreToolUse hook's exit 2:
60
+ every write of the session, the one that mends the line among them. It is
61
+ now one line on stderr, `the hook's line cannot be read (...), so the
62
+ guard is off and this write is not checked`, with the parser's words
63
+ between the brackets, and exit 1, which Claude Code shows as a hook error
64
+ while the write goes on, as for a `--root` that is not a directory. Exit 2
65
+ from the hook is for a write it could not check, and `hook git` and every
66
+ other command stop on such a line as before
67
+ ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
68
+ Upgrading: no release wrote more than `hook claude` on the hook's line. If
69
+ yours holds more and cannot be read, it held every write and now holds
70
+ none: mend the line the hook error names. A hook entry that sets
71
+ `"onFailure": "block"` has Claude Code hold the write for exit 1 too.
72
+
73
+ ### Added
74
+
75
+ - **The context packet lists the rules of the rest where spec-guard refuses
76
+ for a spec pattern that matched no file.** spec-guard 0.22.0 exits 2 for
77
+ a pattern among its `specs` that matches no file beside others that do,
78
+ and still answers with the rules of the files that matched. `context` and
79
+ `start_round` read that as rules that could not be read and listed none.
80
+ They now list those rules under one sentence that names the pattern and
81
+ says a rule kept under it is not listed, and name the patterns in
82
+ `--format json` and in `start_round` as `unmatchedSpecs`, an empty list
83
+ where there is none. Any other refusal reads as it did, and so does a
84
+ spec-guard older than 0.22.0, which writes no such field
85
+ ([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)).
86
+
12
87
  ## 0.13.0
13
88
 
14
89
  `hook claude` says a `--root` on its line that is not a directory, with
package/README.md CHANGED
@@ -101,6 +101,8 @@ The packet an agent starts a round with: the brief in full - it is the contract
101
101
 
102
102
  Rules spec-guard could not read are named as unread, with what it said, never as none; a repository where no spec file matches spec-guard's patterns has none.
103
103
 
104
+ A spec pattern of spec-guard's that matches no file beside others that do is neither. spec-guard 0.22.0 and later refuse the question for it, exit 2, and still answer with the rules of every spec file that did match. The packet lists those rules as it lists any, under one sentence that names the pattern: `` spec-guard's pattern `rfcs/*.md` matched no spec file, so a rule kept under it is not listed here; remove the pattern from spec-guard's `specs`. `` `context --format json` and `start_round` name the patterns as `unmatchedSpecs`, an empty list where spec-guard names none. An older spec-guard says nothing of such a pattern, and its rules are listed as they were.
105
+
104
106
  **What it could not read is named, never left out.** A cited document whose front matter opens on its first line and is never closed is one spec-core's scanner reads as having no front matter: its status was not read, and the packet says so, with the fix - close the block with `---` on a line of its own - rather than showing a document without one. So is each line of closed front matter that is not `key: value`, such as `status accepted` without its colon, or an indented line with no key above it: spec-core's reader passes over it, so a status written on it was not read, and the packet names the document, the line as written and spec-core's reason. `context --format json` and `start_round` list them as `unreadableFrontMatter`, each with its `path`, `line`, `text` and `reason`. The notes fail nothing; `context` exits 0.
105
107
 
106
108
  A pattern the guard cannot read - malformed, too large to compile, or naming no path, such as `{./,src}` - is named as unread where it is listed, with spec-core's reason, the one the guard gives when it passes over it:
@@ -133,6 +135,7 @@ Paths are compared as the filesystem spells them, links resolved and case correc
133
135
  - `spec-harness hook git` is a pre-commit hook for any agent or none. `init --git-hook` writes it; in a hook of your own it is the line `node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git`.
134
136
  - `hook claude` passes over whatever else is on its line that it does not read, as every release has, where every other command [refuses it](#an-input-that-names-nothing): its line is in settings a later release cannot rewrite, and exit 2 from it holds every write.
135
137
  - A `--root` on the hook's line that is a file, or is not there, is a guard that was set up and is off: `hook claude` says so on stderr and exits 1, which Claude Code shows as a hook error while the write goes on. It refuses nothing for it. No release wrote a `--root` there; without one the hook reads the project Claude Code names, then the session's directory, and outside a git work tree it has nothing to guard and says nothing.
138
+ - A line the hook cannot read - an option no command has, an option left without its value, a `--format` or an `--at` that is none - is said the same way: one line on stderr, exit 1, and no write held for it. Exit 2 from `hook claude` is for a write it could not check, never for what is wrong with its own line. `hook git` stops the commit on such a line, exit 2, where a person reads why.
136
139
 
137
140
  A guard is a guardrail - an agent that writes through a shell passes it - so the audit and spec-brief's archive are the gates ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
138
141
 
@@ -371,6 +374,13 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
371
374
  - A `cert-authority` line is a certificate authority's key, not a person's, and is left out.
372
375
  - A line OpenSSH refuses is named as no signer, with why. Its quotes are read as OpenSSH reads them: in the principals, the first double quote, wherever it opens, is taken out with the next, which ends the field; an option's value is always in double quotes, `\"` a quote inside it. And its options are OpenSSH's: `cert-authority`, `namespaces`, `valid-after` and `valid-before`, separated by commas.
373
376
  - **How Claude Code runs the guard**: the Claude Code plugin, with the settings file that turns it on; `init`'s hooks and server; both, which guards every write twice and exits 1, with how to keep one; or neither.
377
+ - A hook in Claude Code's settings is the guard by its command, `spec-harness` or its script, and the words `hook claude`, whatever else is on its line. `doctor` reads the line as the hook reads it, through the same code, with the project in place of `${CLAUDE_PROJECT_DIR}`, and says what the hook does with it.
378
+ - A line that holds more than `init` writes, such as `--root ${CLAUDE_PROJECT_DIR}` or `--brief 012`, is said as it is and fails nothing: `"hook claude --root ${CLAUDE_PROJECT_DIR}" in .claude/settings.json is more than init writes, and guards: the hook reads the line, and its root, /work/app, is a directory`.
379
+ - A line the hook cannot read, and a `--root` that is a file or is not there, are a guard that is off. Each is said in the words the hook itself says before a write, and exits 1.
380
+ - A line with no `args` is one Claude Code hands to a shell. Where it holds a quote, a variable or anything else a shell rewrites after its command, what the hook is handed is *cannot tell*, never fine; `--strict` fails it. Written with `args`, each word reaches the hook as it stands, and `doctor` reads it.
381
+ - `"onFailure": "block"` on the hook's entry is said in a note. Claude Code 2.1.295 or later then holds a write when the hook fails, where without the field the write goes ahead: by the hook's exit 1 for a question it cannot read, by a hook that does not start, or by one that runs past its timeout. With a line that turns the guard off, that is every write, the one that mends the line among them.
382
+ - `--format json` has each line under `claudeCode.lines`: its `file`, `events`, the `line` as written, its `state` (`guards`, `off` or `unknown`), `onFailure`, and what is said of it as `detail` and `note`, `null` where there is nothing to say.
383
+ - The plugin's own hooks are not read: they are a release's files, and no release wrote more than `hook claude` on the line.
374
384
  - **Which Claude Code**, when Claude Code runs the guard: `claude --version`, found on `PATH` and run without a shell.
375
385
  - Older than 2.1.139 exits 1. An older release ignores a hook's `args` and runs a bare `node`, which reads the hook's input as a script and fails, and a PreToolUse hook that fails with anything but exit 2 blocks nothing: every write passes unguarded.
376
386
  - On Windows, npm installs Claude Code as a `claude.cmd` shim, which cannot be run without a shell, so it is read instead: the `node_modules\@anthropic-ai\claude-code` it runs, as npm's shim or pnpm's names it, and that package.json's `version`, which doctor says it read. Nothing is run.
@@ -407,9 +417,14 @@ An option, an argument or a value that is given and names nothing is refused: ex
407
417
 
408
418
  `--help` and `--version` answer whatever else is on the line. Three things are read another way, each for its reason:
409
419
 
410
- - **`hook claude` reads its line as every release has.** The line is in `.claude/settings.json` and in the Claude Code plugin of every release since 0.1.0, which a later release cannot rewrite, and exit 2 from it holds every write of a session, the one that would mend the line among them. So what it does not read is passed over as before, an empty `--brief` with the rest; an option no command has is exit 2 there, as it always was. The releases have handed the harness five command lines, `hook claude`, `hook git`, `mcp`, and `mcp --root` with the project or with `.`, and each is read as the release that wrote it read it ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
420
+ - **`hook claude` reads its line as every release has.** The line is in `.claude/settings.json` and in the Claude Code plugin of every release since 0.1.0, which a later release cannot rewrite, and exit 2 from it holds every write of a session, the one that would mend the line among them. So what it does not read is passed over as before, an empty `--brief` with the rest. The releases have handed the harness five command lines, `hook claude`, `hook git`, `mcp`, and `mcp --root` with the project or with `.`, and each is read as the release that wrote it read it ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
421
+
422
+ Two things on its line are said, and still refuse nothing. Each is one line on stderr and exit 1, said where there is a write the line leaves unchecked. Claude Code does not block a tool call on exit 1: it goes on with the write and shows the line as a hook error in the transcript. No release wrote more than `hook claude` on the hook's line, so either is a line a person changed.
423
+
424
+ - **A `--root` that is a file, or is not there.** It read as a session outside a git work tree, exit 0 and no word, and the guard was off. It is `--root is "nowhere", which is not a directory, so the guard is off and this write is not checked`, with what to do. An empty `--root` is the directory the hook was started in, and guards there, as before.
425
+ - **A line the parser cannot read**: an option no command has, as in `hook claude --nope`; an option left without its value, as `--root $DIR` is in a shell where the variable is not set; a `--format` or an `--at` that is none. Each was exit 2, as it is from every other command, and Claude Code blocks the tool call on a PreToolUse hook's exit 2: every write of the session waited on a line only a write can mend. It is `the hook's line cannot be read (Option '--root <value>' argument missing), so the guard is off and this write is not checked`, with what to do. The parser's own words stand between the brackets, on one line.
411
426
 
412
- One thing on its line is said, and still refuses nothing: a `--root` that is a file, or is not there. It read as a session outside a git work tree, exit 0 and no word, and the guard was off. It is now one line on stderr and exit 1, `--root is "nowhere", which is not a directory, so the guard is off and this write is not checked`, with what to do. Claude Code does not block a tool call on exit 1: it goes on with the write and shows the line as a hook error in the transcript. No release wrote a `--root` on the hook's line, so this is a line a person changed. An empty `--root` is the directory the hook was started in, and guards there, as before.
427
+ Exit 2 from `hook claude` is kept for a write it could not check: a configuration that does not load, briefs that cannot be read, a brief that is named and is not one, an error nothing expected. `hook git` is not read this way. It stops one commit, exit 2, on a line a person reads, who can then mend the hook. Nor is a line that does not say `hook claude`, such as `hook` alone: the harness cannot tell whose it is. A hook entry that sets `"onFailure": "block"`, which Claude Code 2.1.295 and later read, has Claude Code hold the action when the hook fails, and exit 1 is a failure to it; neither `init` nor the plugin writes that field. [`doctor`](#doctor) says each of these of a line it finds in Claude Code's settings, before any write meets it.
413
428
  - **`doctor` says an empty `--brief` or `--base` in its report**, as it says a brief spec-brief does not know: a diagnosis is what a person runs when something is wrong, and it reports what is wrong with what it was given.
414
429
  - **`SPEC_BRIEF` set and empty is as unset**, since what sets it sets it whether or not there is a round ([Which brief](#which-brief)).
415
430
 
@@ -489,7 +504,7 @@ A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-har
489
504
  | Code | Means |
490
505
  | --- | --- |
491
506
  | `0` | Clean. |
492
- | `1` | Refused, found something, or waiting on a person. |
507
+ | `1` | Refused, found something, or waiting on a person. From `hook claude`, also what it says and holds no write for: a question it cannot read, and a line of its own it cannot read or whose `--root` is no directory. |
493
508
  | `2` | The answer cannot be trusted, or did not arrive. An option, an argument or a value that [names nothing](#an-input-that-names-nothing) is one. A git command that failed is one, and so is a stdout its reader closed before all of the output was written, as a pipeline into `head` does: each is one line on stderr. An error the harness did not expect is one too: `spec-harness: unexpected error:` and its stack, on stderr. |
494
509
  | `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
495
510
  | `143` | A `probe` interrupted by SIGTERM. |
package/dist/answers.d.ts CHANGED
@@ -26,6 +26,12 @@ export interface QueryRule {
26
26
  }
27
27
  /** The rules in each of the results of `spec-guard query --json`, in order. */
28
28
  export declare function readQueryRules(document: unknown): QueryRule[][];
29
+ /**
30
+ * The spec patterns `spec-guard query --json` says matched no file, as
31
+ * written. A document that says nothing of them names none: spec-guard has
32
+ * written the field since 0.22.0, and only where there is such a pattern.
33
+ */
34
+ export declare function readQueryUnmatched(document: unknown): string[];
29
35
  export interface GuardResult {
30
36
  readonly ok: boolean;
31
37
  readonly description: string;
package/dist/answers.js CHANGED
@@ -70,6 +70,16 @@ export function readQueryRules(document) {
70
70
  });
71
71
  });
72
72
  }
73
+ /**
74
+ * The spec patterns `spec-guard query --json` says matched no file, as
75
+ * written. A document that says nothing of them names none: spec-guard has
76
+ * written the field since 0.22.0, and only where there is such a pattern.
77
+ */
78
+ export function readQueryUnmatched(document) {
79
+ const read = reader('spec-guard query');
80
+ const top = read.object(document, 'a document');
81
+ return read.list(top['unmatchedSpecs'], 'unmatchedSpecs').map((value, index) => read.text(value, `unmatchedSpecs[${index}]`));
82
+ }
73
83
  /** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */
74
84
  export function readGuardRun(document) {
75
85
  const read = reader('spec-guard');
@@ -124,7 +134,7 @@ export function readArchivePlan(document) {
124
134
  const file = read.optionalText(reason['file'], `${where}.file`);
125
135
  const line = read.optionalNumber(reason['line'], `${where}.line`);
126
136
  const path = read.optionalText(reason['path'], `${where}.path`);
127
- return {
137
+ const carried = {
128
138
  rule: read.text(reason['rule'], `${where}.rule`),
129
139
  severity: severity,
130
140
  message: read.text(reason['message'], `${where}.message`),
@@ -133,6 +143,7 @@ export function readArchivePlan(document) {
133
143
  line,
134
144
  path,
135
145
  };
146
+ return carried;
136
147
  });
137
148
  return { blocking: reasons('blocking'), warnings: reasons('warnings') };
138
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What spec-guard and spec-brief answer in JSON, besides the brief list\n * briefs.ts reads: a query's rules, a run's results and errors, and an\n * archive's plan. Each is checked for the fields the harness reads, with the\n * types it reads them as, and a document of another shape is an error that\n * names the tool and the field (ADR-0002): the command that asked stops with\n * exit 2, as for any sibling that printed something it cannot read, rather\n * than crash halfway through the document or count what it could not read as\n * clean. Fields the harness does not read are not checked, so a sibling may\n * add to its documents freely.\n */\n\nimport type { ArchiveReason } from './audit.js';\nimport { SiblingOutputError } from './briefs.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads fields of one document, naming the tool, its command and the field in every refusal. */\nfunction reader(printer: string) {\n const refuse = (where: string, expected: string): never => {\n throw new SiblingOutputError(`${printer} printed ${where} that is not ${expected}`);\n };\n return {\n object(value: unknown, where: string): Json {\n return isObject(value) ? value : refuse(where, 'an object');\n },\n /** A list that is absent is an empty one. */\n list(value: unknown, where: string): unknown[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? (value as unknown[]) : refuse(where, 'a list');\n },\n text(value: unknown, where: string): string {\n return typeof value === 'string' ? value : refuse(where, 'text');\n },\n optionalText(value: unknown, where: string): string | undefined {\n return value === undefined ? undefined : typeof value === 'string' ? value : refuse(where, 'text');\n },\n number(value: unknown, where: string): number {\n return typeof value === 'number' ? value : refuse(where, 'a number');\n },\n optionalNumber(value: unknown, where: string): number | undefined {\n return value === undefined ? undefined : typeof value === 'number' ? value : refuse(where, 'a number');\n },\n flag(value: unknown, where: string): boolean {\n return typeof value === 'boolean' ? value : refuse(where, 'true or false');\n },\n refuse,\n };\n}\n\n/** Where spec-guard places a directive. */\nexport interface Spec {\n readonly file: string;\n readonly line: number;\n}\n\nexport interface QueryRule {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n /** Absent from a document that says nothing of it, which is in force. */\n readonly inForce: boolean;\n}\n\n/** The rules in each of the results of `spec-guard query --json`, in order. */\nexport function readQueryRules(document: unknown): QueryRule[][] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['results'], 'results').map((value, index) => {\n const result = read.object(value, `results[${index}]`);\n return read.list(result['rules'], `results[${index}].rules`).map((ruleValue, at) => {\n const where = `results[${index}].rules[${at}]`;\n const rule = read.object(ruleValue, where);\n const reason = rule['reason'];\n if (reason !== undefined && reason !== null && typeof reason !== 'string') read.refuse(`${where}.reason`, 'text or null');\n const inForce = rule['inForce'];\n return {\n document: read.text(rule['document'], `${where}.document`),\n line: read.number(rule['line'], `${where}.line`),\n kind: read.text(rule['kind'], `${where}.kind`),\n description: read.text(rule['description'], `${where}.description`),\n reason: typeof reason === 'string' ? reason : null,\n inForce: inForce === undefined ? true : read.flag(inForce, `${where}.inForce`),\n };\n });\n });\n}\n\nexport interface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec | undefined;\n}\n\nexport interface GuardError {\n readonly message: string;\n readonly raw: string;\n readonly spec?: Spec | undefined;\n}\n\n/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */\nexport function readGuardRun(document: unknown): { results: GuardResult[]; errors: GuardError[] } {\n const read = reader('spec-guard');\n const top = read.object(document, 'a document');\n const spec = (value: unknown, where: string): Spec | undefined => {\n if (value === undefined) return undefined;\n const placed = read.object(value, where);\n return { file: read.text(placed['file'], `${where}.file`), line: read.number(placed['line'], `${where}.line`) };\n };\n const results = read.list(top['results'], 'results').map((value, index) => {\n const where = `results[${index}]`;\n const result = read.object(value, where);\n return {\n ok: read.flag(result['ok'], `${where}.ok`),\n description: read.text(result['description'], `${where}.description`),\n message: read.text(result['message'], `${where}.message`),\n spec: spec(result['spec'], `${where}.spec`),\n };\n });\n const errors = read.list(top['errors'], 'errors').map((value, index) => {\n const where = `errors[${index}]`;\n const error = read.object(value, where);\n return {\n message: read.text(error['message'], `${where}.message`),\n raw: read.optionalText(error['raw'], `${where}.raw`) ?? '',\n spec: spec(error['spec'], `${where}.spec`),\n };\n });\n return { results, errors };\n}\n\nconst SEVERITIES: readonly string[] = ['error', 'warning', 'note'];\n\n/**\n * What `spec-brief archive --dry-run --format json` would refuse and warn\n * about. A document with no plan plans nothing to refuse. spec-brief gives a\n * reason without a hint where it has no next step, and the reason is carried\n * as it gave it: a field it left out is undefined, which JSON leaves out too.\n */\nexport function readArchivePlan(document: unknown): { blocking: ArchiveReason[]; warnings: ArchiveReason[] } {\n const read = reader('spec-brief archive');\n const top = read.object(document, 'a document');\n if (top['plan'] === undefined) return { blocking: [], warnings: [] };\n const plan = read.object(top['plan'], 'plan');\n const reasons = (key: 'blocking' | 'warnings'): ArchiveReason[] =>\n read.list(plan[key], `plan.${key}`).map((value, index) => {\n const where = `plan.${key}[${index}]`;\n const reason = read.object(value, where);\n const severity = read.text(reason['severity'], `${where}.severity`);\n if (!SEVERITIES.includes(severity)) read.refuse(`${where}.severity`, 'error, warning or note');\n const hint = read.optionalText(reason['hint'], `${where}.hint`);\n const file = read.optionalText(reason['file'], `${where}.file`);\n const line = read.optionalNumber(reason['line'], `${where}.line`);\n const path = read.optionalText(reason['path'], `${where}.path`);\n return {\n rule: read.text(reason['rule'], `${where}.rule`),\n severity: severity as ArchiveReason['severity'],\n message: read.text(reason['message'], `${where}.message`),\n hint,\n file,\n line,\n path,\n } as ArchiveReason;\n });\n return { blocking: reasons('blocking'), warnings: reasons('warnings') };\n}\n"]}
1
+ {"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What spec-guard and spec-brief answer in JSON, besides the brief list\n * briefs.ts reads: a query's rules, a run's results and errors, and an\n * archive's plan. Each is checked for the fields the harness reads, with the\n * types it reads them as, and a document of another shape is an error that\n * names the tool and the field (ADR-0002): the command that asked stops with\n * exit 2, as for any sibling that printed something it cannot read, rather\n * than crash halfway through the document or count what it could not read as\n * clean. Fields the harness does not read are not checked, so a sibling may\n * add to its documents freely.\n */\n\nimport type { ArchiveReason } from './audit.js';\nimport { SiblingOutputError } from './briefs.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads fields of one document, naming the tool, its command and the field in every refusal. */\nfunction reader(printer: string) {\n const refuse = (where: string, expected: string): never => {\n throw new SiblingOutputError(`${printer} printed ${where} that is not ${expected}`);\n };\n return {\n object(value: unknown, where: string): Json {\n return isObject(value) ? value : refuse(where, 'an object');\n },\n /** A list that is absent is an empty one. */\n list(value: unknown, where: string): unknown[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? (value as unknown[]) : refuse(where, 'a list');\n },\n text(value: unknown, where: string): string {\n return typeof value === 'string' ? value : refuse(where, 'text');\n },\n optionalText(value: unknown, where: string): string | undefined {\n return value === undefined ? undefined : typeof value === 'string' ? value : refuse(where, 'text');\n },\n number(value: unknown, where: string): number {\n return typeof value === 'number' ? value : refuse(where, 'a number');\n },\n optionalNumber(value: unknown, where: string): number | undefined {\n return value === undefined ? undefined : typeof value === 'number' ? value : refuse(where, 'a number');\n },\n flag(value: unknown, where: string): boolean {\n return typeof value === 'boolean' ? value : refuse(where, 'true or false');\n },\n refuse,\n };\n}\n\n/** Where spec-guard places a directive. */\nexport interface Spec {\n readonly file: string;\n readonly line: number;\n}\n\nexport interface QueryRule {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n /** Absent from a document that says nothing of it, which is in force. */\n readonly inForce: boolean;\n}\n\n/** The rules in each of the results of `spec-guard query --json`, in order. */\nexport function readQueryRules(document: unknown): QueryRule[][] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['results'], 'results').map((value, index) => {\n const result = read.object(value, `results[${index}]`);\n return read.list(result['rules'], `results[${index}].rules`).map((ruleValue, at) => {\n const where = `results[${index}].rules[${at}]`;\n const rule = read.object(ruleValue, where);\n const reason = rule['reason'];\n if (reason !== undefined && reason !== null && typeof reason !== 'string') read.refuse(`${where}.reason`, 'text or null');\n const inForce = rule['inForce'];\n return {\n document: read.text(rule['document'], `${where}.document`),\n line: read.number(rule['line'], `${where}.line`),\n kind: read.text(rule['kind'], `${where}.kind`),\n description: read.text(rule['description'], `${where}.description`),\n reason: typeof reason === 'string' ? reason : null,\n inForce: inForce === undefined ? true : read.flag(inForce, `${where}.inForce`),\n };\n });\n });\n}\n\n/**\n * The spec patterns `spec-guard query --json` says matched no file, as\n * written. A document that says nothing of them names none: spec-guard has\n * written the field since 0.22.0, and only where there is such a pattern.\n */\nexport function readQueryUnmatched(document: unknown): string[] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['unmatchedSpecs'], 'unmatchedSpecs').map((value, index) => read.text(value, `unmatchedSpecs[${index}]`));\n}\n\nexport interface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec | undefined;\n}\n\nexport interface GuardError {\n readonly message: string;\n readonly raw: string;\n readonly spec?: Spec | undefined;\n}\n\n/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */\nexport function readGuardRun(document: unknown): { results: GuardResult[]; errors: GuardError[] } {\n const read = reader('spec-guard');\n const top = read.object(document, 'a document');\n const spec = (value: unknown, where: string): Spec | undefined => {\n if (value === undefined) return undefined;\n const placed = read.object(value, where);\n return { file: read.text(placed['file'], `${where}.file`), line: read.number(placed['line'], `${where}.line`) };\n };\n const results = read.list(top['results'], 'results').map((value, index) => {\n const where = `results[${index}]`;\n const result = read.object(value, where);\n return {\n ok: read.flag(result['ok'], `${where}.ok`),\n description: read.text(result['description'], `${where}.description`),\n message: read.text(result['message'], `${where}.message`),\n spec: spec(result['spec'], `${where}.spec`),\n };\n });\n const errors = read.list(top['errors'], 'errors').map((value, index) => {\n const where = `errors[${index}]`;\n const error = read.object(value, where);\n return {\n message: read.text(error['message'], `${where}.message`),\n raw: read.optionalText(error['raw'], `${where}.raw`) ?? '',\n spec: spec(error['spec'], `${where}.spec`),\n };\n });\n return { results, errors };\n}\n\nconst SEVERITIES: readonly string[] = ['error', 'warning', 'note'];\n\n/**\n * What `spec-brief archive --dry-run --format json` would refuse and warn\n * about. A document with no plan plans nothing to refuse. spec-brief gives a\n * reason without a hint where it has no next step, and the reason is carried\n * as it gave it: a field it left out is undefined, which JSON leaves out too.\n */\nexport function readArchivePlan(document: unknown): { blocking: ArchiveReason[]; warnings: ArchiveReason[] } {\n const read = reader('spec-brief archive');\n const top = read.object(document, 'a document');\n if (top['plan'] === undefined) return { blocking: [], warnings: [] };\n const plan = read.object(top['plan'], 'plan');\n const reasons = (key: 'blocking' | 'warnings'): ArchiveReason[] =>\n read.list(plan[key], `plan.${key}`).map((value, index) => {\n const where = `plan.${key}[${index}]`;\n const reason = read.object(value, where);\n const severity = read.text(reason['severity'], `${where}.severity`);\n if (!SEVERITIES.includes(severity)) read.refuse(`${where}.severity`, 'error, warning or note');\n const hint = read.optionalText(reason['hint'], `${where}.hint`);\n const file = read.optionalText(reason['file'], `${where}.file`);\n const line = read.optionalNumber(reason['line'], `${where}.line`);\n const path = read.optionalText(reason['path'], `${where}.path`);\n const carried = {\n rule: read.text(reason['rule'], `${where}.rule`),\n severity: severity as ArchiveReason['severity'],\n message: read.text(reason['message'], `${where}.message`),\n hint,\n file,\n line,\n path,\n };\n return carried as ArchiveReason;\n });\n return { blocking: reasons('blocking'), warnings: reasons('warnings') };\n}\n"]}
package/dist/cli.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { type CliIO } from './workspace.js';
13
13
  export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
14
- export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo. Neither\n raises a request: beside one, only --root and --format are read.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note <text>. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies\n and the install scripts it allowed.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at <base|head|both>, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice, or Claude Code is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n For context, guard, hook, escalate, rulings, audit, probe, premises\n and doctor.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch. For context,\n guard, hook, rulings, audit, probe and doctor.\n --root <dir> Run from another directory, which must exist. For every command.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n For every command but hook and mcp.\n --strict Warnings fail the run. For guard, audit, premises and doctor.\n --help, --version\n\nAn option a command does not read, an argument it does not take, an option or an\nargument given an empty value, and a brief given both as the argument and as --brief\nare refused, exit 2, not read as if they were not there. doctor says an empty --brief\nor --base in its report. hook claude reads its line as every release has, and says a\n--root that is not a directory with exit 1, refusing nothing.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
14
+ export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo. Neither\n raises a request: beside one, only --root and --format are read.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note <text>. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies\n and the install scripts it allowed.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at <base|head|both>, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice or its hook's line turns it off, or Claude Code\n is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n For context, guard, hook, escalate, rulings, audit, probe, premises\n and doctor.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch. For context,\n guard, hook, rulings, audit, probe and doctor.\n --root <dir> Run from another directory, which must exist. For every command.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n For every command but hook and mcp.\n --strict Warnings fail the run. For guard, audit, premises and doctor.\n --help, --version\n\nAn option a command does not read, an argument it does not take, an option or an\nargument given an empty value, and a brief given both as the argument and as --brief\nare refused, exit 2, not read as if they were not there. doctor says an empty --brief\nor --base in its report. hook claude reads its line as every release has, and says a\nline it cannot read, and a --root that is not a directory, with exit 1: it refuses\nnothing for either.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
15
15
  /**
16
16
  * Runs the command line and resolves to the exit code, for an error nothing
17
17
  * below expected too.
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import { mcpCommand } from './server.js';
24
24
  import { gitHook, initCommand } from './setup.js';
25
25
  import { notFido2, readAllowedSigners } from './signers.js';
26
26
  import { SiblingError } from './siblings.js';
27
- import { emptyValue, isBlank, noDirectory, refusal } from './usage.js';
27
+ import { emptyValue, isBlank, isClaudeHook, readHookLine, refusal, unreadHookLine } from './usage.js';
28
28
  import { checkClaudeCode, CLAUDE_CODE_MINIMUM, MINIMUM_VERSIONS } from './versions.js';
29
29
  import { activeBrief, describeActive, EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, namedId, openWorkspace, parseOptions, UsageError, version, } from './workspace.js';
30
30
  export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './workspace.js';
@@ -60,7 +60,8 @@ Commands:
60
60
  doctor Which siblings are installed, at which versions, which brief is
61
61
  named, how Claude Code runs the guard, and git's hook. Exit 1
62
62
  when a sibling is older than this release needs, the guard is
63
- installed twice, or Claude Code is too old to run its hooks.
63
+ installed twice or its hook's line turns it off, or Claude Code
64
+ is too old to run its hooks.
64
65
 
65
66
  Options:
66
67
  --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.
@@ -79,7 +80,8 @@ An option a command does not read, an argument it does not take, an option or an
79
80
  argument given an empty value, and a brief given both as the argument and as --brief
80
81
  are refused, exit 2, not read as if they were not there. doctor says an empty --brief
81
82
  or --base in its report. hook claude reads its line as every release has, and says a
82
- --root that is not a directory with exit 1, refusing nothing.
83
+ line it cannot read, and a --root that is not a directory, with exit 1: it refuses
84
+ nothing for either.
83
85
 
84
86
  Exit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.
85
87
  `;
@@ -129,47 +131,70 @@ async function readStdin(io) {
129
131
  chunks.push(chunk);
130
132
  return Buffer.concat(chunks).toString('utf8');
131
133
  }
134
+ /**
135
+ * The write Claude Code asks the hook about, or the exit code that answers
136
+ * already: 1 for a question that cannot be read, 0 for a tool that writes
137
+ * no file. Asked before anything on the hook's own line is, so that a line
138
+ * that is wrong is said where there is a write it leaves unchecked.
139
+ */
140
+ async function writeAsked(io) {
141
+ let question;
142
+ try {
143
+ question = await readStdin(io);
144
+ }
145
+ catch (error) {
146
+ // A question that could not be read is one the hook cannot see, as one
147
+ // it cannot parse is, below: it has no event to hold a write by.
148
+ io.stderr.write(`spec-harness: the hook input could not be read: ${error.message}\n`);
149
+ return EXIT_FAILED;
150
+ }
151
+ const request = parseClaudeHook(question);
152
+ if ('error' in request) {
153
+ // A hook that cannot read its question refuses nothing it cannot see,
154
+ // but says so where the person reads it: a non-blocking error.
155
+ io.stderr.write(`spec-harness: ${request.error}\n`);
156
+ return EXIT_FAILED;
157
+ }
158
+ return request.paths.length === 0 ? EXIT_OK : request;
159
+ }
160
+ /**
161
+ * What is wrong with the hook's own line, said and nothing refused for it:
162
+ * one line, since Claude Code shows the first line of the stderr of a hook
163
+ * that exits 1 and goes on with the write, where exit 2 would hold every
164
+ * write of the session, the one that mends the line among them (ADR-0005).
165
+ */
166
+ function guardOff(io, { wrong, mend }) {
167
+ io.stderr.write(`spec-harness: ${wrong}, so the guard is off and this write is not checked; ${mend}\n`);
168
+ return EXIT_FAILED;
169
+ }
170
+ /**
171
+ * Claude Code's hook on a line the parser refused: an option no command has,
172
+ * an option left without its value, a --format or an --at that is none. It
173
+ * was exit 2 before the question was read, for the line a settings file
174
+ * holds as for one typed at a terminal. What is said of it is `doctor`'s
175
+ * to say as well, of a line it finds in a settings file (usage.ts).
176
+ */
177
+ async function unreadLine(said, io) {
178
+ const request = await writeAsked(io);
179
+ if (typeof request === 'number')
180
+ return request;
181
+ return guardOff(io, unreadHookLine(said).fault);
182
+ }
132
183
  async function hookCommand(options, io) {
133
184
  const kind = options.positionals[0];
134
185
  if (kind === 'claude') {
135
- let question;
136
- try {
137
- question = await readStdin(io);
138
- }
139
- catch (error) {
140
- // A question that could not be read is one the hook cannot see, as one
141
- // it cannot parse is, below: it has no event to hold a write by.
142
- io.stderr.write(`spec-harness: the hook input could not be read: ${error.message}\n`);
143
- return EXIT_FAILED;
144
- }
145
- const request = parseClaudeHook(question);
146
- if ('error' in request) {
147
- // A hook that cannot read its question refuses nothing it cannot see,
148
- // but says so where the person reads it: a non-blocking error.
149
- io.stderr.write(`spec-harness: ${request.error}\n`);
150
- return EXIT_FAILED;
151
- }
152
- if (request.paths.length === 0)
153
- return EXIT_OK;
154
- // An empty root is the directory the hook was started in: git starts
155
- // there when it is handed no directory, so the hook has guarded there
156
- // for an empty root in every release.
157
- const root = options.root === '' ? io.cwd : options.root;
158
- // A root written on the hook's line that is a file, or is not there: git
159
- // starts in neither, which read as a session outside a work tree, exit 0
160
- // and no word, and the guard someone had configured was off. It is said,
161
- // and refuses nothing: exit 1 is to Claude Code an error that does not
162
- // block, whose first line of stderr it shows the person, where exit 2
163
- // would hold every write of the session, the one that mends the line
164
- // among them (ADR-0005).
165
- if (root !== undefined && !isDirectory(root)) {
166
- io.stderr.write(`spec-harness: ${noDirectory(root)}, so the guard is off and this write is not checked; give the hook's line a directory that exists, or leave --root out for the project Claude Code names\n`);
167
- return EXIT_FAILED;
168
- }
186
+ const request = await writeAsked(io);
187
+ if (typeof request === 'number')
188
+ return request;
189
+ // The root the line gives, or what is wrong with it: read as `doctor`
190
+ // reads the line where it finds it in a settings file (usage.ts).
191
+ const line = readHookLine(options.root, io.cwd, isDirectory);
192
+ if (line.state === 'off')
193
+ return guardOff(io, line.fault);
169
194
  const cwd = request.cwd ?? io.cwd;
170
195
  let workspace;
171
196
  try {
172
- workspace = await openWorkspace({ ...options, root: root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);
197
+ workspace = await openWorkspace({ ...options, root: line.root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);
173
198
  }
174
199
  catch (error) {
175
200
  if (error instanceof UsageError)
@@ -278,6 +303,10 @@ async function doctorCommand(options, io) {
278
303
  // and registers the server twice (ADR-0012).
279
304
  const wiring = await claudeCodeWiring(workspace.root, io.env);
280
305
  const claude = wiringState(wiring);
306
+ // What the hook does with each line the settings hold for it, where there
307
+ // is something to say: more on it than init writes, a line that turns the
308
+ // guard off, one only a shell can read, and what onFailure changes.
309
+ const lineNotes = wiring.lines.flatMap((line) => [line.detail, line.note]).filter((note) => note !== null);
281
310
  // Only a Claude Code that runs the guard is asked its release: one older
282
311
  // than the hooks need lets every write through unguarded.
283
312
  const release = claude === 'none' ? null : checkClaudeCode(await claudeVersion(io.env));
@@ -324,6 +353,7 @@ async function doctorCommand(options, io) {
324
353
  ...more(signerNotes),
325
354
  `plugin ${describePlugin(plugin)}`,
326
355
  `claude ${describeClaudeCode(wiring)}`,
356
+ ...more(lineNotes),
327
357
  ...more(release === null ? [] : [describeClaudeRelease(release)]),
328
358
  `git ${describeGitHook(hook)}`,
329
359
  ...more(hookNote === null ? [] : [hookNote]),
@@ -336,13 +366,15 @@ async function doctorCommand(options, io) {
336
366
  // An outdated sibling is a problem to fix, where a missing optional one is
337
367
  // a choice; with none installed, the harness can check nothing. A double
338
368
  // install is a problem too, where no Claude Code wiring at all is a choice,
339
- // and so is a Claude Code too old to run the hooks. One whose release
340
- // cannot be told is not fine either: --strict fails it.
369
+ // and so are a hook whose own line turns the guard off and a Claude Code
370
+ // too old to run the hooks. A release that cannot be told is not fine
371
+ // either, nor a line only a shell can read: --strict fails both.
341
372
  const outdated = rows.some((row) => row.state === 'outdated');
342
373
  const usable = rows.some((row) => row.state === 'found');
374
+ const off = wiring.lines.some((line) => line.state === 'off');
343
375
  const tooOld = release?.state === 'outdated';
344
- const untold = options.strict && release?.state === 'unknown';
345
- return outdated || !usable || claude === 'both' || tooOld || untold ? EXIT_FAILED : EXIT_OK;
376
+ const untold = options.strict && (release?.state === 'unknown' || wiring.lines.some((line) => line.state === 'unknown'));
377
+ return outdated || !usable || claude === 'both' || off || tooOld || untold ? EXIT_FAILED : EXIT_OK;
346
378
  }
347
379
  /* ---------------------------------------------------------------- dispatch */
348
380
  /**
@@ -373,7 +405,13 @@ async function dispatch(argv, io) {
373
405
  options = parseOptions(argv);
374
406
  }
375
407
  catch (error) {
376
- io.stderr.write(`spec-harness: ${error.message}\n`);
408
+ // The parser throws nothing but its refusal of the line, which holds
409
+ // whose line it was. Claude Code's hook says its own, and holds no
410
+ // write for it.
411
+ const refused = error;
412
+ if (isClaudeHook(refused.words[0], refused.words[1]))
413
+ return unreadLine(refused.message, io);
414
+ io.stderr.write(`spec-harness: ${refused.message}\n`);
377
415
  return EXIT_ERROR;
378
416
  }
379
417
  if (options.version) {