@descent-vtt/spec-harness 0.6.0 → 0.8.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 (87) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +118 -0
  4. package/README.md +275 -24
  5. package/dist/audit.d.ts +71 -1
  6. package/dist/audit.js +140 -17
  7. package/dist/audit.js.map +1 -1
  8. package/dist/branch.d.ts +13 -0
  9. package/dist/branch.js +19 -0
  10. package/dist/branch.js.map +1 -1
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +53 -13
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands.js +12 -4
  15. package/dist/commands.js.map +1 -1
  16. package/dist/config.js +3 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/configure.d.ts +62 -3
  19. package/dist/configure.js +121 -5
  20. package/dist/configure.js.map +1 -1
  21. package/dist/context.d.ts +23 -7
  22. package/dist/context.js +69 -21
  23. package/dist/context.js.map +1 -1
  24. package/dist/document.d.ts +15 -2
  25. package/dist/document.js.map +1 -1
  26. package/dist/formats.d.ts +31 -0
  27. package/dist/formats.js +127 -0
  28. package/dist/formats.js.map +1 -0
  29. package/dist/git.d.ts +6 -0
  30. package/dist/git.js +8 -0
  31. package/dist/git.js.map +1 -1
  32. package/dist/guard.d.ts +8 -0
  33. package/dist/guard.js +19 -1
  34. package/dist/guard.js.map +1 -1
  35. package/dist/host.d.ts +15 -0
  36. package/dist/host.js +111 -0
  37. package/dist/host.js.map +1 -0
  38. package/dist/index.d.ts +4 -2
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/manifests.d.ts +8 -1
  42. package/dist/manifests.js +16 -6
  43. package/dist/manifests.js.map +1 -1
  44. package/dist/premises.d.ts +3 -0
  45. package/dist/premises.js +52 -14
  46. package/dist/premises.js.map +1 -1
  47. package/dist/reader.js +28 -3
  48. package/dist/reader.js.map +1 -1
  49. package/dist/round.d.ts +8 -0
  50. package/dist/round.js +70 -23
  51. package/dist/round.js.map +1 -1
  52. package/dist/server.js +10 -3
  53. package/dist/server.js.map +1 -1
  54. package/dist/setup.d.ts +10 -0
  55. package/dist/setup.js +73 -32
  56. package/dist/setup.js.map +1 -1
  57. package/dist/siblings.d.ts +2 -0
  58. package/dist/siblings.js +1 -1
  59. package/dist/siblings.js.map +1 -1
  60. package/dist/signers.d.ts +46 -0
  61. package/dist/signers.js +180 -0
  62. package/dist/signers.js.map +1 -0
  63. package/dist/types.d.ts +7 -0
  64. package/dist/types.js.map +1 -1
  65. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
  66. package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
  67. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  68. package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
  69. package/dist/vendor/spec-core/pattern/glob.js +207 -17
  70. package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
  71. package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
  72. package/dist/vendor/spec-core/pattern/index.js +1 -1
  73. package/dist/vendor/spec-core/pattern/index.js.map +1 -1
  74. package/dist/vendor/spec-core/text/index.d.ts +1 -0
  75. package/dist/vendor/spec-core/text/index.js +1 -0
  76. package/dist/vendor/spec-core/text/index.js.map +1 -1
  77. package/dist/vendor/spec-core/text/width.d.ts +30 -0
  78. package/dist/vendor/spec-core/text/width.js +113 -0
  79. package/dist/vendor/spec-core/text/width.js.map +1 -0
  80. package/dist/versions.d.ts +40 -1
  81. package/dist/versions.js +33 -1
  82. package/dist/versions.js.map +1 -1
  83. package/dist/workspace.d.ts +6 -1
  84. package/dist/workspace.js +17 -6
  85. package/dist/workspace.js.map +1 -1
  86. package/package.json +1 -1
  87. package/skills/close-round/SKILL.md +4 -1
@@ -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.6.0"
13
+ "version": "0.8.0"
14
14
  }
15
15
  ]
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-harness",
3
- "version": "0.6.0",
3
+ "version": "0.8.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
@@ -1,5 +1,123 @@
1
1
  # Changelog
2
2
 
3
+ spec-harness is before 1.0, and its version says what an upgrade can do to a
4
+ run: a minor release (0.6.x to 0.7.0) may turn a passing run red or change
5
+ what a script reads - a finding on by default that was not, a changed exit
6
+ code, a flag, key or field renamed or removed, each deprecated with a warning
7
+ for a minor release first - and brings new features, while a patch release
8
+ only fixes, reports less or documents, so `^0.7.0` takes only releases that
9
+ cannot turn a run red. The family's policy is
10
+ [spec-core's ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md).
11
+
12
+ ## 0.8.0
13
+
14
+ The context packet names front matter lines it could not read, `doctor`
15
+ reads the release of a Claude Code installed with npm on Windows, and
16
+ spec-harness reads status words in English only again.
17
+
18
+ ### Added
19
+
20
+ - `context` names each line of a cited document's front matter that is not
21
+ `key: value`, with its line number and reason, and says a status on it was
22
+ not read; `--format json` and `start_round` gain `unreadableFrontMatter`
23
+ ([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)).
24
+
25
+ ### Removed
26
+
27
+ - `context` no longer reads a cited document's status under `狀態` or
28
+ `状态`. Upgrading: write the status in English, e.g. `status: superseded`.
29
+
30
+ ### Fixed
31
+
32
+ - `doctor` reads the release of an npm-installed Claude Code on Windows from
33
+ the package its `claude.cmd` shim runs, without running the shim, where it
34
+ could not tell and failed `--strict`
35
+ ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
36
+ - `doctor` says it cannot tell the release of a `claude.exe` Windows cannot
37
+ start, where it stopped with `spawn UNKNOWN`.
38
+ - `doctor` reads the allowed-signers file's quotes and options as OpenSSH
39
+ does, and notes a line OpenSSH refuses as no signer
40
+ ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
41
+ - `init` and `doctor` resolve the spec-brief plugin export as Node does: the
42
+ first matching condition in written order, and a `null` target as not
43
+ exported.
44
+
45
+ ## 0.7.0
46
+
47
+ The audit and `premises` no longer pass on what they could not read, the
48
+ audit says what it measured, both write GitLab, SARIF and GitHub reports,
49
+ `doctor` checks the Claude Code that runs the guard, and a rooted pattern is
50
+ marked wherever it protects, allows or scopes nothing. spec-core is at
51
+ 5666c96.
52
+
53
+ ### Added
54
+
55
+ - `audit` and `premises` take `--format gitlab`, `sarif` and `github`. A
56
+ fingerprint is the rule, the file and the finding's subject, never its
57
+ message or line ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
58
+ - `audit` prints what it measured above the counts line, which stays last,
59
+ and `--format json` and `audit_round` gain `measured`. A brief with no
60
+ assertion says `goals: none declared`.
61
+ - `--format json` findings carry a `subject`; `doctor --format json` gains
62
+ `branchSource`, `allowedSigners.notFido2`, `allowedSigners.problems`,
63
+ `claudeCode.release` and `gitHook`. No `schemaVersion` moves.
64
+ - `doctor` notes signers whose key is not a FIDO2 key (a note, never a
65
+ failure), and says whether git's pre-commit hook runs spec-harness; `init`
66
+ advises that hook when `--git-hook` is not given.
67
+ - The README covers non-Node repositories, merge settings that keep signed
68
+ rulings valid on GitHub and GitLab, a marketplace from a GitLab mirror, and
69
+ the git 2.31 the harness has always needed.
70
+
71
+ ### Changed
72
+
73
+ - spec-core at 5666c96: `.//docs` is `docs`, as POSIX reads it, in every
74
+ scope, protection, ruling path and manifest name, where it was rooted.
75
+ Upgrading: check any pattern written `.//x`; `//x` is still rooted.
76
+ - `audit` reports an assertion spec-guard cannot read as
77
+ `assertion-unreadable`, where it dropped it and passed; it fails
78
+ `--strict`. Upgrading: fix the directive spec-guard names.
79
+ - `premises` reports an unreadable premise the same way, counts it, and
80
+ `premises --strict` fails on it. Upgrading: fix the directive, or run
81
+ without `--strict`.
82
+ - A leading `/` roots a pattern, so a rooted protection or ruling path
83
+ protects or allows nothing and a rooted manifest name names nothing: the
84
+ packet marks them, and `audit` warns `protection-rooted`,
85
+ `ruling-path-rooted` or `manifest-name-rooted`, failing `--strict`.
86
+ Upgrading: write the pattern without the leading slash.
87
+ - `doctor` exits 1 when the Claude Code wired to the guard is older than
88
+ 2.1.139, which runs every write unguarded; one it cannot find or run is
89
+ unknown, never fine, and fails `--strict`
90
+ ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
91
+ Upgrading: `claude update`.
92
+ - `context` shows a cited document's status under `狀態` or `状态` when it
93
+ has no `status` key. Upgrading: nothing; write the key with an ASCII colon.
94
+
95
+ ### Fixed
96
+
97
+ - On a detached HEAD, as in CI, the brief is found from the forge's branch
98
+ variable (`GITHUB_HEAD_REF`, `GITHUB_REF_NAME` on a branch push,
99
+ `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME`, `CI_COMMIT_BRANCH`), so `premises`
100
+ no longer fails a round's own CI run
101
+ ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
102
+ - A rooted pattern in `affectedFiles` no longer makes spec-guard refuse the
103
+ whole scope; the rules over the other patterns come back.
104
+ - `init --git-hook` writes the hook where git runs it, for an absolute or
105
+ `~` `core.hooksPath` too.
106
+ - `init` names the configured allowed-signers file, suggests a FIDO2 key,
107
+ and says how GitHub, GitLab Premium and GitLab Free protect the rule files.
108
+ - A spec-brief plugin loaded by path is recognised, so `init` no longer adds
109
+ it twice.
110
+ - The `tools` error suggests a `node` command, which Windows can start
111
+ without a shell, where it suggested `npx`.
112
+ - A brief's id is dropped from its title before a full-width colon or an
113
+ unspaced dash too, as spec-brief does.
114
+
115
+ ### Documentation
116
+
117
+ - The README is reorganised into lists and tables, and names the spec-brief
118
+ plugin apart from the Claude Code plugin; SECURITY.md says how to report a
119
+ vulnerability privately.
120
+
3
121
  ## 0.6.0
4
122
 
5
123
  The context packet names a protection or a ruling's path the guard cannot
package/README.md CHANGED
@@ -4,15 +4,22 @@
4
4
 
5
5
  spec-harness is the agent-facing member of the [spec-\* tools](https://github.com/DescentVTT/spec-core#the-family). It calls no model and writes no content - the agent is the model. It gives the agent the contract and the facts, and answers, the same way every time, whether the agent stayed inside the lines. No runtime dependencies, no account, no network.
6
6
 
7
+ The family's words - brief, round, wave, ruling, premise, the two kinds of plugin - are defined in [concepts](https://github.com/DescentVTT/spec-core/blob/main/docs/concepts.md), and the [tutorial](https://github.com/DescentVTT/spec-core/blob/main/docs/tutorial.md) takes one round from the brief to the archive in ten steps.
8
+
7
9
  ```bash
8
10
  npm install --save-dev @descent-vtt/spec-harness @descent-vtt/spec-brief
9
11
  npx spec-harness init # the plan: what it would configure, and why
10
12
  npx spec-harness init --write # apply it
11
13
  ```
12
14
 
13
- spec-brief 0.2.0 or later is required: the brief is the contract, spec-brief is its reader, and 0.2.0 is the first whose archive asks this package's plugin about signed rulings. spec-guard 0.12.0 and spec-graph 0.9.0 or later are used when they are installed, and their absence is reported, never assumed clean. A sibling installed below its minimum is never run: `doctor` and every command that needed it name the minimum ([ADR-0011](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0011-releases-are-staged-by-ci.md)).
15
+ ## Requirements
16
+
17
+ - **spec-brief 0.2.0 or later**, required: the brief is the contract, spec-brief is its reader, and 0.2.0 is the first whose archive asks this package's spec-brief plugin about signed rulings.
18
+ - **spec-guard 0.12.0 and spec-graph 0.9.0 or later**, used when they are installed. Their absence is reported, never assumed clean.
19
+ - **Claude Code 2.1.139 or later** runs the guard hooks and the Claude Code plugin's server: the hooks pass the project's path in a hook's `args`, which Claude Code reads from that release on, and the plugin's server names `${CLAUDE_PROJECT_DIR}`, which a Claude Code plugin may from the same release ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)). An older Claude Code runs every write unguarded, so `doctor` checks the one on `PATH`.
20
+ - **git 2.31 or later**: spec-harness asks git where its shared directory and its hooks are with `rev-parse --path-format`, which that release added (2021).
14
21
 
15
- Claude Code 2.1.139 or later runs the guard hooks and the plugin's server: the hooks pass the project's path in a hook's `args`, which Claude Code reads from that release on, and the plugin's server names `${CLAUDE_PROJECT_DIR}`, which a plugin may from the same release ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
22
+ A sibling installed below its minimum is never run: `doctor` and every command that needed it name the minimum ([ADR-0011](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0011-releases-are-staged-by-ci.md)).
16
23
 
17
24
  ## A round
18
25
 
@@ -30,17 +37,81 @@ person approves brief 012 ─▶ agent on branch brief/012-rotate-tokens
30
37
  spec-harness audit ─▶ spec-brief archive --dry-run ─▶ person archives
31
38
  ```
32
39
 
40
+ ### Which brief
41
+
42
+ The first of these names the round's brief; it is never guessed ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)):
43
+
44
+ 1. `--brief <id>`;
45
+ 2. `SPEC_BRIEF`;
46
+ 3. the branch name: `brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}`.
47
+
48
+ CI checks out a commit on no branch, so on a detached HEAD the branch is the one the forge's CI names, the first of:
49
+
50
+ | Variable | Set by |
51
+ | --- | --- |
52
+ | `GITHUB_HEAD_REF` | a GitHub pull request |
53
+ | `GITHUB_REF_NAME`, when `GITHUB_REF_TYPE` is `branch` | a GitHub push |
54
+ | `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | a GitLab merge request pipeline |
55
+ | `CI_COMMIT_BRANCH` | a GitLab branch pipeline |
56
+
57
+ A branch checked out always wins, and `doctor` says which variable named the branch. Without them, a round's own CI run would name no brief, and `premises` would report the premise the round retires as stale.
58
+
59
+ ## Commands
60
+
61
+ In the order a round meets them; `init` and `doctor` set a repository up and check it.
62
+
33
63
  ### `context [brief]`
34
64
 
35
- The brief in full - it is the contract - then what it implies: the files the round may write and must not change as the guard reads them, the rulings already signed, the briefs it depends on and whether they are done, the architecture rules spec-guard holds that code to, and the documents the brief links to, whole, until the packet reaches its budget (`context.budget`, 60,000 characters). Documents left out are named by path, not dropped. So is a document whose front matter opens on its first line and is never closed, which spec-core's scanner reads as 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. The note fails nothing; `context` exits 0. 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. The rules are those over the scope: spec-guard is asked about where `affectedFiles` can reach, `src` for `src/**` or `src/`, not the whole repository. A name with no glob syntax that exists, `src` or `src/a.ts`, is asked about as itself; one the round has yet to create is asked about through the directory that will hold it, the root for a name at the top, since it may become a directory as well as a file ([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)). A pattern in `affectedFiles` the guard cannot read - malformed, too large to compile, or naming no path, such as `{./,src}` - is named as unread where the scope is listed, with spec-core's reason, the one the guard gives when it passes over it: it puts no path in the scope, and spec-guard is not asked about it. When no pattern in `affectedFiles` can be read, the packet says the scope could not be read, never that spec-guard holds no rule over it. `context --format json` and `start_round` list such patterns as `unreadableScope`. A protection the guard cannot read is named the same way where the protections are listed, with what it does: until it is fixed, the guard refuses every write but to the brief, and no ruling waives the refusal. So is a path of a signed ruling the guard cannot read, where the ruling is listed: the guard passes over it, so it allows nothing. They are listed as `unreadableProtections`, each with its `pattern` and `reason`, and `unreadableRulingPaths`, each with its `ruling` as well. `--base` names the base the rulings are verified against.
65
+ The packet an agent starts a round with: the brief in full - it is the contract - then what it implies:
66
+
67
+ - the files the round may write and must not change, as the guard reads them;
68
+ - the rulings already signed;
69
+ - the briefs it depends on, and whether they are done;
70
+ - the architecture rules spec-guard holds that code to;
71
+ - the documents the brief links to, whole, until the packet reaches its budget (`context.budget`, 60,000 characters). Documents left out are named by path, not dropped. Each is headed by its first level-one heading and the status its front matter gives under `status`, in English as every spec-* tool reads it, whatever language the document is written in.
72
+
73
+ `--base` names the base the rulings are verified against.
74
+
75
+ **The rules over the scope.** spec-guard is asked about where `affectedFiles` can reach, not the whole repository ([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)):
76
+
77
+ - for a pattern, the directory it reaches: `src` for `src/**` or `src/`;
78
+ - for a name with no glob syntax that exists, `src` or `src/a.ts`, that name;
79
+ - for a name the round has yet to create, the directory that will hold it, the root for a name at the top, since it may become a directory as well as a file.
80
+
81
+ 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.
82
+
83
+ **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.
84
+
85
+ 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:
86
+
87
+ | Unreadable in | What the packet says of it | In `context --format json` and `start_round` |
88
+ | --- | --- | --- |
89
+ | `affectedFiles` | It puts no path in the scope, and spec-guard is not asked about it. When no pattern in `affectedFiles` can be read, the packet says the scope could not be read, never that spec-guard holds no rule over it. | `unreadableScope`, each with its `pattern` and `reason` |
90
+ | `protectedFiles` | Until it is fixed, the guard refuses every write but to the brief, and no ruling waives the refusal. | `unreadableProtections`, each with its `pattern` and `reason` |
91
+ | a signed ruling's paths | The guard passes over it, so it allows nothing. | `unreadableRulingPaths`, each with its `pattern`, `reason` and `ruling` |
92
+
93
+ A pattern a leading `/` roots at the filesystem's root, alone or on a brace alternative, such as `/docs` or `{/docs,src/**}`, can be read, and names no path the guard decides, all of which are repository-relative. Where it is listed, it is marked:
94
+
95
+ - in the scope, as putting no path in it: spec-guard is not asked about it, so the rules over the rest of the scope stand;
96
+ - as a protection, as protecting no path;
97
+ - as a ruling's path, as allowing nothing.
36
98
 
37
99
  ### `guard <path...>` and the hooks
38
100
 
39
- May the round write this? A path the brief protects is **refused** unless a signed ruling allows it; a path in `affectedFiles` is allowed; anything else is **outside the scope** - a warning by default, or a question to the person, or a refusal (`outOfScope`: `warn`, `ask`, `deny`). Paths are compared as the filesystem spells them, links resolved and case corrected, so `SRC/db/schema.ts` does not walk past a protection on `src/db/schema.ts` on Windows.
101
+ May the round write this?
102
+
103
+ | The path | The answer |
104
+ | --- | --- |
105
+ | one the brief protects | **Refused**, unless a signed ruling allows it. |
106
+ | one in `affectedFiles` | Allowed. |
107
+ | anything else | **Outside the scope**: a warning by default, or a question to the person, or a refusal (`outOfScope`: `warn`, `ask`, `deny`). |
40
108
 
41
- `spec-harness hook claude` answers Claude Code's PreToolUse and PostToolUse hooks: a refusal before the write, with the reason and the next step; a warning after a write outside the scope. It never answers `allow`, which would skip the person's own permission prompt. `spec-harness hook git` is a pre-commit hook for any agent or none. 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)).
109
+ Paths are compared as the filesystem spells them, links resolved and case corrected, so `SRC/db/schema.ts` does not walk past a protection on `src/db/schema.ts` on Windows.
42
110
 
43
- **Which brief?** `--brief <id>`, then `SPEC_BRIEF`, then the branch name (`brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}`). Never guessed ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
111
+ - `spec-harness hook claude` answers Claude Code's PreToolUse and PostToolUse hooks: a refusal before the write, with the reason and the next step; a warning after a write outside the scope. It never answers `allow`, which would skip the person's own permission prompt.
112
+ - `spec-harness hook git` is a pre-commit hook for any agent or none.
113
+
114
+ 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)).
44
115
 
45
116
  ### `escalate`, `rule`, `rulings`
46
117
 
@@ -57,13 +128,96 @@ npx spec-harness rule E-012-1 --allow --note "Add rotated_at only."
57
128
  git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
58
129
  ```
59
130
 
60
- A ruling is a row in the brief's `## Rulings` table. It **counts** when the commit that last changed the row is signed by a key the **base branch's** `.github/allowed_signers` lists. An agent can write a row and compute any hash; it cannot produce the person's signature, and an edit to the row moves it to a commit that must be signed again ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)). Use a FIDO2 key (`ed25519-sk`) where the agent runs as you: its signature needs a touch no process can supply.
131
+ A ruling is a row in the brief's `## Rulings` table. It **counts** when the commit that last changed the row is signed by a key the **base branch's** `.github/allowed_signers` lists. The file is `rulings.allowedSigners` in `.spec-harness.json`, `.github/allowed_signers` by default.
132
+
133
+ - An agent can write a row and compute any hash; it cannot produce the person's signature, and an edit to the row moves it to a commit that must be signed again ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
134
+ - Use a FIDO2 key (`ssh-keygen -t ed25519-sk`) where the agent runs as you: its signature needs a touch no process can supply.
135
+
136
+ **The spec-brief plugin.** spec-brief's archive refuses a round that changed a protected file, and learns that a signed ruling allows it only from this package's spec-brief plugin: `init` adds `"plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"]` to spec-brief's configuration.
137
+
138
+ - Without it, the archive refuses the file whatever was signed.
139
+ - `doctor` says whether spec-brief loads the spec-brief plugin, and `audit` names it as the reason for such a refusal.
140
+ - spec-brief loads a plugin by a path as well, one that starts with `.` or is absolute: a path to this package's spec-brief plugin file counts as loading it.
61
141
 
62
- spec-brief's archive refuses a round that changed a protected file, and learns that a signed ruling allows it only from this package's plugin: `init` adds `"plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"]` to spec-brief's configuration. Without it, the archive refuses the file whatever was signed; `doctor` says whether spec-brief loads the plugin, and `audit` names it as the reason for such a refusal.
142
+ #### Merge a round with a merge commit
143
+
144
+ A squash or a rebase writes new commits: the forge signs them with its own key or not at all, so on the base branch a ruling's row blames to a commit no allowed signer signed, and it no longer counts. Rulings are verified on the round's branch, before the merge; keep them verifiable after it:
145
+
146
+ - GitHub: allow merge commits for the repository, and turn off *Allow squash merging* and *Allow rebase merging* in its settings, or in the rules for the branch rounds merge into.
147
+ - GitLab: in *Settings > Merge requests*, set *Merge method* to *Merge commit* and *Squash commits when merging* to *Do not allow*. *Merge commit with semi-linear history* and *Fast-forward merge* need the branch rebased, which the *Rebase* button does on the server, writing new commits.
63
148
 
64
149
  ### `audit [brief]`
65
150
 
66
- One report: what spec-brief's archive would refuse or warn about, run with `--dry-run`; the brief's own assertions through spec-guard - a goal that fails, or a premise (under a heading such as *The Defect, Measured*) that still holds after the round meant to change it; rulings whose signatures do not verify; and every dependency the round added to `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements*.txt`, NuGet project files or a `Gemfile`. A part that could not be measured is a finding, never a silence. So is a name in `dependencies.manifests` that spec-core's glob cannot read, malformed or too large to compile: the audit names it and the reason in a warning, `manifest-name-unread`, which fails the audit only under `--strict`, and reads the manifests the other names name.
151
+ One report on a round, in four parts:
152
+
153
+ | Part | What it reports |
154
+ | --- | --- |
155
+ | The archive | What spec-brief's archive would refuse or warn about, run with `--dry-run`. |
156
+ | Assertions | The brief's own assertions through spec-guard: a goal that fails, or a premise (under a heading such as *The Defect, Measured*) that still holds after the round meant to change it. |
157
+ | Rulings | Rulings whose signatures do not verify. |
158
+ | Dependencies | Every dependency the round added to `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements*.txt`, NuGet project files or a `Gemfile`. |
159
+
160
+ #### What it could not measure
161
+
162
+ A part that could not be measured is a finding, never a silence. Each of these is a warning, which fails the audit only under `--strict`:
163
+
164
+ - `assertion-unreadable`: an assertion in the brief spec-guard cannot read, on its line, with spec-guard's reason. Nothing it states was run.
165
+ - `manifest-name-unread`: a name in `dependencies.manifests` that spec-core's glob cannot read, malformed or too large to compile, with the reason. The audit reads the manifests the other names name.
166
+ - `protection-rooted` and `ruling-path-rooted`: a pattern in `protectedFiles`, or a path of a signed ruling, that a leading `/` roots, alone or on a brace alternative, such as `/src/db/schema.ts`. It protects no path, so the round could change the file it meant to protect, or allows nothing, so the file is still refused. Write the protection without the slash; a ruling's path is fixed by a new ruling.
167
+ - `manifest-name-rooted`: a name a leading `/` roots at the filesystem's root, alone or on a brace alternative, such as `/package.json` or `{/Gemfile,Cargo.toml}`. Every path the audit reads is repository-relative, so the rooted part names no manifest; write the name without the slash, since a name is matched at any depth.
168
+
169
+ #### What it measured
170
+
171
+ What was measured is said beside what was found, so an audit that found nothing can be told from one that checked nothing ([spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md)). It is the line above the counts, which stay the last line:
172
+
173
+ ```text
174
+ measured: goals: 2 held, 0 failed · premises: 1 retired, 0 holding · archive: asked · rulings: none · dependencies: 3 changed, 0 unread
175
+ 0 error(s), 0 warning(s), 1 note(s)
176
+ ```
177
+
178
+ A brief that declares no assertion says `goals: none declared`, and draws no warning: a brief without assertions is a brief, and the archive still measures its round.
179
+
180
+ `--format json` has the same as `measured`, beside `counts`, and `audit_round` says it too:
181
+
182
+ | Field | Holds |
183
+ | --- | --- |
184
+ | `changes` | `measured` or `unmeasured` |
185
+ | `archive` | `asked` or `unavailable` |
186
+ | `assertions` | `run` or `unavailable` |
187
+ | `goals` | counts: `held`, `failed` |
188
+ | `premises` | counts: `retired`, `holding` |
189
+ | `unreadableAssertions` | a count |
190
+ | `rulings` | counts: `verified`, `unverified` |
191
+ | `dependencies` | counts: `changed`, `unread` |
192
+
193
+ #### For a forge
194
+
195
+ `--format gitlab`, `sarif` or `github` prints the findings for a forge:
196
+
197
+ | Format | Prints | An error, a warning, a note |
198
+ | --- | --- | --- |
199
+ | `gitlab` | a GitLab Code Quality report | `major`, `minor`, `info` |
200
+ | `sarif` | SARIF 2.1.0 | levels `error`, `warning`, `note` |
201
+ | `github` | GitHub workflow commands | `error`, `warning`, `notice` |
202
+
203
+ - Each carries the finding's hint after its message.
204
+ - A finding with no file of its own is placed on the brief, and one with no line on line 1.
205
+ - The fingerprint is the SHA-256 of the rule, the file and the finding's `subject` - the assertion, ruling, dependency or name it is about - never of its message or line, so a reworded message or a line added above does not read as one problem fixed and another found.
206
+ - SARIF carries the fingerprint as `partialFingerprints.specHarnessFinding`, with what was measured as a note.
207
+
208
+ In GitLab CI:
209
+
210
+ ```yaml
211
+ spec-harness:
212
+ image: node:22
213
+ script:
214
+ - npm ci
215
+ - npx spec-harness audit --format gitlab > gl-spec-harness.json
216
+ artifacts:
217
+ when: always
218
+ reports:
219
+ codequality: gl-spec-harness.json
220
+ ```
67
221
 
68
222
  ### `probe [brief]`
69
223
 
@@ -82,11 +236,20 @@ signature: expected 401, got 200
82
236
  ```
83
237
  ````
84
238
 
85
- `probe --at base` runs it in a temporary worktree at the base commit: every run must fail **for the declared reason** - the `signature` in the output, or a JUnit `test` failing - or the verdict is `vacuous` (no defect), `flaky` or `invalid`. `probe --at head` must be green: `fixed`. The evidence table it prints names the hash of the probe it measured with ([ADR-0007](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0007-probes-declare-their-failure.md)). Interrupted, it stops every command it started, with everything those started, before it removes the worktree ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
239
+ - `probe --at base` runs it in a temporary worktree at the base commit: every run must fail **for the declared reason** - the `signature` in the output, or a JUnit `test` failing - or the verdict is `vacuous` (no defect), `flaky` or `invalid`.
240
+ - `probe --at head` must be green: `fixed`.
241
+ - The evidence table it prints names the hash of the probe it measured with ([ADR-0007](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0007-probes-declare-their-failure.md)).
242
+ - Interrupted, it stops every command it started, with everything those started, before it removes the worktree ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
86
243
 
87
244
  ### `premises`
88
245
 
89
- Is every live brief still about something true? It runs spec-guard over the live briefs' assertions and reports each premise - an assertion under a section in `assertions.premises` - that no longer holds as `stale-premise`: the defect was fixed another way, or the code the brief describes is gone, and an agent sent to fix it would fix nothing. Goals are left to `audit`, since a goal fails until its round is done. For the same reason, the premise of the brief a round is working on - the one `--brief`, `SPEC_BRIEF` or the branch names - is reported as `audit` reports it, `premise-retired`, a note that fails nothing: on the round's branch, a premise that no longer holds is the work being done. Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not there to ask.
246
+ Is every live brief still about something true? It runs spec-guard over the live briefs' assertions and reports each premise - an assertion under a section in `assertions.premises` - that no longer holds as `stale-premise`: the defect was fixed another way, or the code the brief describes is gone, and an agent sent to fix it would fix nothing.
247
+
248
+ - Goals are left to `audit`, since a goal fails until its round is done.
249
+ - For the same reason, the premise of the brief a round is working on - the one `--brief`, `SPEC_BRIEF` or the branch names - is reported as `audit` reports it, `premise-retired`, a note that fails nothing: on the round's branch, a premise that no longer holds is the work being done.
250
+ - A premise spec-guard cannot read is `assertion-unreadable`, a warning, as in the audit: nothing checks it, so `--strict` fails the run on it, and the summary counts it apart from the premises that were checked.
251
+
252
+ Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not there to ask. `--format gitlab`, `sarif` or `github` prints its findings for a forge, as `audit` does.
90
253
 
91
254
  ### `mcp`
92
255
 
@@ -94,28 +257,105 @@ Is every live brief still about something true? It runs spec-guard over the live
94
257
 
95
258
  ### `init`
96
259
 
97
- Configures the family to agree. It prints the plan, merges into files that exist, keeping what is there, and changes nothing without `--write`:
98
-
99
- - **spec-brief**: `spec-brief init` when there is no configuration, then this package's plugin in its `plugins`, `"@descent-vtt/spec-harness/spec-brief-plugin"`, and the base below as its `archiving.base` unless one is set. spec-brief's archive asks the plugin whether a signed ruling allows a protected file, and refuses the file without it; and without a base it checks no protected file at all, warning only that the scope went unmeasured, so a plain `spec-brief archive` is a gate only once one is named.
100
- - **spec-graph**, when it is installed: the archive in its `historyPatterns`, so an archived brief is a record whatever its status says, and a live brief that depends on one is not a stale premise. That holds only where spec-graph reads the briefs, and its default patterns (`docs/`, `doc/`, `adr/`, `rfcs/`, `specs/` and Markdown at the root) do not reach `briefs/`: `init` says whether they do, and leaves adding the briefs to `patterns` to you, since that changes what spec-graph checks, and its `patterns` replace its defaults. It merges into the configuration spec-graph reads, and leaves a `"spec-graph"` key in `package.json` to you.
101
- - **`.spec-harness.json`**, naming the base rounds are measured from and the allowed signers are read on: the remote's default branch where git recorded one, as a clone does and git 2.48 or later does on a fetch; otherwise the branch `init` runs on when it is `main` or `master`, or the only branch, since a repository made with `git init` and pushed to a remote often records no default. When it cannot tell, it says so and writes none: set `"base"` yourself.
102
- - **Claude Code**: the guard hooks in `.claude/settings.json` and the MCP server in `.mcp.json`, each run with `node` from the project's `node_modules`. These are the ones the [plugin](#as-a-claude-code-plugin) brings, so while the plugin is on - `"spec-harness@<marketplace>": true` under `enabledPlugins` in your user, project or local Claude Code settings - `init` writes neither and says why (`skip`). To have its entries instead, which every clone reads, turn the plugin off for the project, `claude plugin disable spec-harness@spec-tools --scope local`, and run `init` again. It removes nothing: where the plugin is on and a file already holds its entry, it says so (`advise`) and leaves the file alone. The `npx` entries 0.1 wrote are replaced.
103
- - With `--git-hook`, a pre-commit hook.
260
+ Configures the family to agree. It prints the plan, merges into files that exist, keeping what is there, and changes nothing without `--write`. Each step names its file and what `init` does there: `skip` is a step it does not take and `advise` one it leaves to you, each with the reason.
261
+
262
+ - **spec-brief**: `spec-brief init` when there is no configuration, then:
263
+ - this package's spec-brief plugin in its `plugins`, `"@descent-vtt/spec-harness/spec-brief-plugin"`. spec-brief's archive asks the plugin whether a signed ruling allows a protected file, and refuses the file without it;
264
+ - the base below as its `archiving.base`, unless one is set. Without a base, the archive checks no protected file at all, warning only that the scope went unmeasured, so a plain `spec-brief archive` is a gate only once one is named.
265
+ - **spec-graph**, when it is installed: the archive in its `historyPatterns`, so an archived brief is a record whatever its status says, and a live brief that depends on one is not a stale premise.
266
+ - That holds only where spec-graph reads the briefs, and its default patterns (`docs/`, `doc/`, `adr/`, `rfcs/`, `specs/` and Markdown at the root) do not reach `briefs/`. `init` says whether they do, and leaves adding the briefs to `patterns` to you, since that changes what spec-graph checks, and its `patterns` replace its defaults.
267
+ - It merges into the configuration spec-graph reads, and leaves a `"spec-graph"` key in `package.json` to you.
268
+ - **`.spec-harness.json`**, naming the base rounds are measured from and the allowed signers are read on:
269
+ - the remote's default branch where git recorded one, as a clone does and git 2.48 or later does on a fetch;
270
+ - otherwise the branch `init` runs on when it is `main` or `master`, or the only branch, since a repository made with `git init` and pushed to a remote often records no default;
271
+ - when it cannot tell, it says so and writes none: set `"base"` yourself.
272
+ - **Claude Code**: the guard hooks in `.claude/settings.json` and the MCP server in `.mcp.json`, each run with `node` from the project's `node_modules`. The `npx` entries 0.1 wrote are replaced.
273
+ - These are the ones the [Claude Code plugin](#as-a-claude-code-plugin) brings, so while that plugin is on - `"spec-harness@<marketplace>": true` under `enabledPlugins` in your user, project or local Claude Code settings - `init` writes neither and says why (`skip`).
274
+ - To have its entries instead, which every clone reads, turn the Claude Code plugin off for the project, `claude plugin disable spec-harness@spec-tools --scope local`, and run `init` again.
275
+ - It removes nothing: where the Claude Code plugin is on and a file already holds its entry, it says so (`advise`) and leaves the file alone.
276
+ - **git's pre-commit hook**, where git runs it from: `core.hooksPath`, relative, absolute or under `~`, and the shared hooks of a linked worktree.
277
+ - It refuses a commit that changes what the active brief protects, for any agent or none, a shell's writes included.
278
+ - With `--git-hook` it is written; without, the plan advises it (`advise`).
279
+ - A hook that exists is never replaced: `init` says to add the line to it.
280
+ - **The allowed-signers file** `rulings.allowedSigners` names, `.github/allowed_signers` by default: one line per person, and a FIDO2 key, `ssh-keygen -t ed25519-sk`, for anyone whose agent runs as them (`advise`).
281
+ - With it, how the forge keeps it, the ADRs, the tool configurations and the CI configuration from changing unread, as [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md) asks; [spec-core's adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md) has the settings:
282
+ - GitHub: CODEOWNERS and a protected branch;
283
+ - GitLab Premium: Code Owners;
284
+ - GitLab Free, which has no Code Owners approval: a protected branch no one pushes to, merged by Maintainers, with agents as Developers and pipelines that must succeed.
104
285
 
105
286
  ### `doctor`
106
287
 
107
- Which sibling tools are installed, at which versions, and how each is run, the repository root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names. Then what a signed ruling needs to count: the base, and whether `--base`, `.spec-harness.json` or the remote named it; whether the allowed-signers file is on that base; and whether spec-brief loads this package's plugin. A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1. A command named under `tools` is run as named, and its version is not checked. Then how Claude Code runs the guard: the 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. The first thing to run when a hook refuses something unexpectedly.
288
+ The first thing to run when a hook refuses something unexpectedly. It reports:
289
+
290
+ - **The siblings**: which are installed, at which versions, and how each is run.
291
+ - A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1.
292
+ - A command named under `tools` is run as named, and its version is not checked.
293
+ - **The repository**: its root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names.
294
+ - **What a signed ruling needs to count**:
295
+ - the base, and whether `--base`, `.spec-harness.json` or the remote named it;
296
+ - whether the allowed-signers file is on that base;
297
+ - whether spec-brief loads this package's spec-brief plugin, by name or by a path to its file.
298
+ - **The signers' keys**: a note naming each signer in the allowed-signers file whose key is not a FIDO2 key (`sk-ssh-ed25519@openssh.com` or `sk-ecdsa-sha2-nistp256@openssh.com`).
299
+ - A note, never a failure: [ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md) also accepts a key the agent's account cannot read, and a PIV or PKCS#11 hardware key reads as a plain `ssh-rsa` or `ecdsa` line.
300
+ - A `cert-authority` line is a certificate authority's key, not a person's, and is left out.
301
+ - 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.
302
+ - **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.
303
+ - **Which Claude Code**, when Claude Code runs the guard: `claude --version`, found on `PATH` and run without a shell.
304
+ - 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.
305
+ - 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.
306
+ - A `claude` that is not found, a Windows script that runs no `@anthropic-ai/claude-code` package whose package.json gives a version, and a version that cannot be read are *cannot tell*, never fine; `--strict` fails them.
307
+ - The Claude Code in an editor or the desktop app may be another release than the one on `PATH`: check it there with `claude --version` or `/status`.
308
+ - **git's pre-commit hook**: whether it runs spec-harness, where git runs it from: installed, missing, a hook of the repository's own without the line, or, outside Windows, one git skips because it is not executable.
108
309
 
109
310
  ## As a Claude Code plugin
110
311
 
111
- The repository is a plugin and a one-plugin marketplace: the four skills (`draft-brief`, `split-goal`, `run-round`, `close-round`), the hooks and the MCP server. The hooks and the server run the `spec-harness` installed in your project, `node ${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`, so install it there first; the server is told the project with `--root`, since Claude Code starts a plugin's server in the plugin's own directory. It needs Claude Code 2.1.139 or later.
312
+ spec-harness ships two different things called a plugin. This section is the **Claude Code plugin**, installed into Claude Code. The **spec-brief plugin**, `@descent-vtt/spec-harness/spec-brief-plugin`, is a module spec-brief's archive loads to learn of signed rulings; `init` configures it ([above](#escalate-rule-rulings)).
313
+
314
+ The repository is a Claude Code plugin and a one-plugin marketplace: the four skills (`draft-brief`, `split-goal`, `run-round`, `close-round`), the hooks and the MCP server. The hooks and the server run the `spec-harness` installed in your project, `node ${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`, so install it there first; the server is told the project with `--root`, since Claude Code starts a plugin's server in the plugin's own directory. It needs Claude Code 2.1.139 or later.
112
315
 
113
316
  ```text
114
317
  /plugin marketplace add DescentVTT/spec-harness
115
318
  /plugin install spec-harness@spec-tools
116
319
  ```
117
320
 
118
- The plugin's hooks and server are the ones `init` writes into `.claude/settings.json` and `.mcp.json`: use one or the other. With both, every write is guarded twice and the server is registered twice. `init` run while the plugin is on writes neither. The other order, `init` first and the plugin after, nothing prevents: Claude Code runs a plugin's hooks as the plugin ships them, and a plugin hook that stood down because it believed `init`'s ran could leave no guard at all - Claude Code reads no hooks from `.claude/settings.json` in a session that spans several repositories, and managed settings can turn off the settings' hooks while those of a plugin they force on keep running ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)). So after installing the plugin where `init` has run, run `npx spec-harness doctor`: a double install exits 1. Then take the `spec-harness` entries out of those two files, or turn the plugin off for the project.
321
+ ### From a mirror
322
+
323
+ `DescentVTT/spec-harness` is GitHub shorthand, fetched from github.com. Where GitHub cannot be reached, as on a company network, mirror the repository and add the mirror by its full git URL; the plugin is `spec-harness@spec-tools` either way:
324
+
325
+ ```text
326
+ /plugin marketplace add https://gitlab.example.com/tools/spec-harness.git
327
+ /plugin install spec-harness@spec-tools
328
+ ```
329
+
330
+ ### The plugin or `init`, not both
331
+
332
+ The Claude Code plugin's hooks and server are the ones `init` writes into `.claude/settings.json` and `.mcp.json`: use one or the other. With both, every write is guarded twice and the server is registered twice.
333
+
334
+ - `init` run while the plugin is on writes neither.
335
+ - The other order, `init` first and the plugin after, nothing prevents: Claude Code runs a plugin's hooks as the plugin ships them.
336
+ - A plugin hook that stood down because it believed `init`'s ran could leave no guard at all: Claude Code reads no hooks from `.claude/settings.json` in a session that spans several repositories, and managed settings can turn off the settings' hooks while those of a plugin they force on keep running ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
337
+
338
+ So after installing the plugin where `init` has run:
339
+
340
+ 1. Run `npx spec-harness doctor`: a double install exits 1.
341
+ 2. Take the `spec-harness` entries out of those two files, or turn the plugin off for the project.
342
+
343
+ ## In a repository that is not Node
344
+
345
+ The hooks, the server and git's hook run the spec-harness installed in the project, so a .NET, Java or Python repository installs the tools as a Node project does, and its build never sees them:
346
+
347
+ 1. A `package.json` at the root holding `{ "private": true }`. `private` keeps npm from ever publishing it.
348
+ 2. `npm install --save-dev @descent-vtt/spec-harness @descent-vtt/spec-brief`, and `@descent-vtt/spec-guard` for the rules and the assertions. npm writes the tools as `devDependencies`, and `package-lock.json` pins them.
349
+ 3. `node_modules/` in `.gitignore`.
350
+ 4. `npm ci` after a clone and in CI, which installs what the lock pins.
351
+
352
+ The hooks name `${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`, not `npx` or a global install:
353
+
354
+ - Claude Code starts a hook wherever the session stands and a plugin's server in the plugin's directory, so neither can rely on `npx` finding the project's install.
355
+ - On Windows `npx` is a shim that cannot start without a shell.
356
+ - The copy in the project is the version the project pinned, the one `doctor` checks the siblings against.
357
+
358
+ A person who runs the tools by hand runs `npx spec-harness`, from the root.
119
359
 
120
360
  ## Configuration
121
361
 
@@ -128,17 +368,28 @@ The plugin's hooks and server are the ones `init` writes into `.claude/settings.
128
368
  | `base` | the remote's default branch | What rounds are measured from, and where allowed signers are read. `init` names it, since a repository git did not clone may have no default branch recorded. |
129
369
  | `rulings.section` | `"Rulings"` | The brief section holding the rulings table. |
130
370
  | `rulings.allowedSigners` | `".github/allowed_signers"` | The allowed-signers file, read from the base branch. |
131
- | `dependencies.manifests` | the list above | Manifest names whose added dependencies the audit reports. A name that cannot be read is a warning in the audit. |
371
+ | `dependencies.manifests` | the list under [`audit`](#audit-brief) | Manifest names whose added dependencies the audit reports. A name that cannot be read is a warning in the audit. |
132
372
  | `context.budget` | `60000` | Characters a context packet may hold; cited documents fill what the rest leaves, and those that do not fit are named by path. |
133
373
  | `assertions.premises` | `The Defect, Measured`, `Premises`, `Preconditions` | Sections whose assertions state what was true before the round. |
134
374
  | `probes.runs`, `probes.timeout` | `2`, `600` | Runs per probe, and seconds per run. |
135
375
  | `tools` | found in `node_modules` | A sibling's command by name: `{ "spec-brief": ["node", "path/to/spec-brief.js"] }`. |
136
376
 
137
- **Exit codes:** `0` clean, `1` refused, found something, or waiting on a person, `2` the answer cannot be trusted. A `probe` interrupted while it runs exits as a shell reports a process the signal ended: `130` on SIGINT (Ctrl+C), `143` on SIGTERM, `129` on SIGHUP.
377
+ **Exit codes:**
378
+
379
+ | Code | Means |
380
+ | --- | --- |
381
+ | `0` | Clean. |
382
+ | `1` | Refused, found something, or waiting on a person. |
383
+ | `2` | The answer cannot be trusted. |
384
+ | `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
385
+ | `143` | A `probe` interrupted by SIGTERM. |
386
+ | `129` | A `probe` interrupted by SIGHUP. |
387
+
388
+ An interrupted `probe` exits as a shell reports a process the signal ended.
138
389
 
139
390
  ## Design
140
391
 
141
- The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/README.md); what the whole family shares is [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md).
392
+ The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/README.md); what the whole family shares is [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md). Working on spec-harness itself: [CONTRIBUTING.md](https://github.com/DescentVTT/spec-harness/blob/main/CONTRIBUTING.md); reporting a vulnerability: [SECURITY.md](https://github.com/DescentVTT/spec-harness/blob/main/SECURITY.md).
142
393
 
143
394
  ## License
144
395