@descent-vtt/spec-harness 0.17.0 → 0.19.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +263 -0
- package/README.md +63 -5
- package/dist/cli.d.ts +15 -1
- package/dist/cli.js +334 -85
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +12 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +158 -18
- package/dist/configure.js +318 -55
- package/dist/configure.js.map +1 -1
- package/dist/fs.d.ts +83 -3
- package/dist/fs.js +190 -13
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +66 -7
- package/dist/git.js +80 -8
- package/dist/git.js.map +1 -1
- package/dist/hooks.d.ts +66 -0
- package/dist/hooks.js +112 -15
- package/dist/hooks.js.map +1 -1
- package/dist/round.d.ts +25 -4
- package/dist/round.js +100 -33
- package/dist/round.js.map +1 -1
- package/dist/setup.d.ts +12 -2
- package/dist/setup.js +92 -26
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +18 -4
- package/dist/siblings.js +30 -17
- package/dist/siblings.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +71 -8
- package/dist/usage.js +108 -19
- package/dist/usage.js.map +1 -1
- package/dist/workspace.d.ts +38 -1
- package/dist/workspace.js +90 -15
- package/dist/workspace.js.map +1 -1
- package/hooks/hooks.json +16 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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,269 @@ 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.19.0
|
|
13
|
+
|
|
14
|
+
An option that takes one value is given one, and a line that is wrong is
|
|
15
|
+
refused with `--help` or `--version` on it, as the family's contract has
|
|
16
|
+
it from 2026-10-18 (spec-core's ADR-0005, and
|
|
17
|
+
[ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)
|
|
18
|
+
here).
|
|
19
|
+
|
|
20
|
+
`doctor` reads a hyphen in a matcher as the Claude Code on `PATH` reads
|
|
21
|
+
it, and no command ends on a stack or fails to come back for what stands
|
|
22
|
+
where a repository or a settings file is read
|
|
23
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
24
|
+
|
|
25
|
+
The hook decides a write in the work tree Claude works in, where a write a
|
|
26
|
+
worktree's brief protects went through unchecked in a worktree session, and
|
|
27
|
+
names the brief of a branch beside a tag of its name; it answers after a
|
|
28
|
+
write from what it decided before it, and says as a session starts what
|
|
29
|
+
would hold every write of it
|
|
30
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md),
|
|
31
|
+
[ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Two values for an option that takes one are refused.** The parser kept
|
|
36
|
+
the last and passed over the earlier with no word: `doctor --root nowhere
|
|
37
|
+
--root .` ran in `.`, `--brief 404 --brief 001` was brief 001, and
|
|
38
|
+
`probe --at sideways --at head` ran at the head. Every command but
|
|
39
|
+
Claude Code's hook now exits 2 with one line that names both, as
|
|
40
|
+
`--root is given twice, as "nowhere" and as ".", and one of the two would
|
|
41
|
+
go unread; give it one`. The two are compared as the option reads them,
|
|
42
|
+
so one directory spelt two ways, or `--brief 12` beside `--brief 012`,
|
|
43
|
+
is the option once. `--path` and `--option` are lists, as before.
|
|
44
|
+
Upgrading: give each such option once; a script that builds a line by
|
|
45
|
+
adding a second `--root` or `--format` to override the first exits 2.
|
|
46
|
+
- **Claude Code's hook says two values for one option on its own line.**
|
|
47
|
+
`hook claude --root nowhere --root .` guarded in `.`, and `hook claude
|
|
48
|
+
--brief 001 --brief 404` held every write for 404. It is a line the hook
|
|
49
|
+
cannot read, said in one line on stderr with exit 1, before a write and
|
|
50
|
+
after one, and holds no write: the guard is off until the line is
|
|
51
|
+
mended. `doctor` says the line in the same words and exits 1. No line a
|
|
52
|
+
release wrote gives an option two values.
|
|
53
|
+
Upgrading: a hook's line with an option given twice, which only a person
|
|
54
|
+
writes, is said and checks no write; give the option once.
|
|
55
|
+
- **A wrong line is refused beside `--help` or `--version`, and the two
|
|
56
|
+
beside each other.** `doctor --help --root a --root b`, `doctor --help
|
|
57
|
+
--note x`, `doctor --help extra`, `doctor --help --root nowhere` and
|
|
58
|
+
`bogus --help` printed the help, exit 0, and `--help --version` printed
|
|
59
|
+
the version. Each is now exit 2 and the line it is refused with without
|
|
60
|
+
either; `--help` beside `--version`, in either order, is
|
|
61
|
+
`--help is given beside --version, and one of the two answers would go
|
|
62
|
+
unread; give one of them`. On `hook claude`'s line it is a line the
|
|
63
|
+
hook cannot read, exit 1. A line that is right answers as before, `guard
|
|
64
|
+
--help` among them.
|
|
65
|
+
Upgrading: ask for one of the two, on a line that is right.
|
|
66
|
+
- **A matcher of names with a hyphen in one is read as the release on
|
|
67
|
+
`PATH` reads it.** By Claude Code's changelog such a matcher matched as a
|
|
68
|
+
substring before 2.1.195, and the changelog does not say how the rest of
|
|
69
|
+
it was read. Under an older release, or one that cannot be told, a line
|
|
70
|
+
under `Edit|Write|my-tool` is *cannot tell* where it said the line
|
|
71
|
+
guards a write by `Edit` and `Write`, and `init` writes no hook beside
|
|
72
|
+
it.
|
|
73
|
+
Upgrading: `doctor --strict` fails such a line under Claude Code 2.1.139
|
|
74
|
+
to 2.1.194 or with no `claude` on `PATH`: take the hyphenated name out of
|
|
75
|
+
the matcher, or update Claude Code.
|
|
76
|
+
- **A hook file the account may not read is said, where `doctor` and `init`
|
|
77
|
+
ended on EACCES and a stack.** One git may not execute is skipped by git,
|
|
78
|
+
and is said and fails nothing; one it may is *cannot tell*, since git runs
|
|
79
|
+
it and a script it cannot read stops every commit. `--format json` has
|
|
80
|
+
the `gitHook.state` `unreadable`.
|
|
81
|
+
Upgrading: `doctor` exits 0, or 1 under `--strict`, and `init` 0, where
|
|
82
|
+
each exited 2: make the file readable.
|
|
83
|
+
- **A write in a worktree is decided by the worktree's brief.** In a
|
|
84
|
+
worktree session started from the desktop app, and after Claude enters a
|
|
85
|
+
worktree, Claude Code names the main checkout as the project and hands the
|
|
86
|
+
hook the worktree as the question's `cwd`; the hook read the main
|
|
87
|
+
checkout's branch, which named no brief, and a write the worktree's brief
|
|
88
|
+
protects passed with no word. Where `cwd` is in another work tree of the
|
|
89
|
+
same repository, that work tree's branch, briefs and configuration now
|
|
90
|
+
decide the paths it holds, with its siblings looked for in its own install
|
|
91
|
+
and then the project's; any other path, and a `cwd` in another repository
|
|
92
|
+
or in none, is decided as before
|
|
93
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
94
|
+
Upgrading: in a worktree session a write the worktree's brief protects is refused, and one outside its scope warned of by that brief; nothing on the hook's line changes.
|
|
95
|
+
- **A branch beside a tag of its name names its brief.** The branch was read
|
|
96
|
+
by git's short name, `heads/<name>` where a tag or a ref `refs/<name>` has
|
|
97
|
+
the name, which no template reads: the hook passed a protected write,
|
|
98
|
+
git's hook a protected commit, and `doctor` said `(none named)`. It is
|
|
99
|
+
read as HEAD names it; a base `init` writes for such a branch is still the
|
|
100
|
+
name git reads as the branch, `heads/main` beside a tag `main`.
|
|
101
|
+
Upgrading: on such a branch the guard refuses what its brief protects.
|
|
102
|
+
- **git's hook in a linked worktree with no install runs the main work
|
|
103
|
+
tree's.** The hook `init` writes refused the commit there; it now reads
|
|
104
|
+
the script from the repository's main work tree after the worktree's own,
|
|
105
|
+
and `hook git` and `doctor` find the siblings there too, `doctor` saying
|
|
106
|
+
whose install each is (`elsewhere` in `--format json`).
|
|
107
|
+
Upgrading: `init` writes the hook once, so one an earlier release wrote still stops such a commit: take it away and run `spec-harness init --git-hook --write`.
|
|
108
|
+
- **The hook answers after a write from what it decided before it.** Claude
|
|
109
|
+
Code starts the hook before a write and after it, and the run after
|
|
110
|
+
decided the write again, at what the run before costs: git, and spec-brief
|
|
111
|
+
on a brief's branch. The run before now keeps what the run after would say
|
|
112
|
+
under the git common directory, named by the question's `session_id` and
|
|
113
|
+
`tool_use_id`, and the run after answers from it with no process started;
|
|
114
|
+
one that finds nothing kept for its call, or another call's, decides as
|
|
115
|
+
before. Under `outOfScope: "ask"` or `"deny"`, where nothing was kept, it
|
|
116
|
+
says nothing with no process started
|
|
117
|
+
([ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
118
|
+
Upgrading: nothing to change. What another process changes between the two runs of one write, the configuration or what spec-brief reads, is read at the next write rather than after this one.
|
|
119
|
+
- **The hook says, as a session starts, what would hold every write of it.**
|
|
120
|
+
A SessionStart entry aimed at `hook claude` exited 1, `the hook input
|
|
121
|
+
names no event or tool`. It now reads the question, however the session
|
|
122
|
+
starts, and says in one line, to the person and to Claude, a configuration
|
|
123
|
+
that does not load, a brief named that spec-brief does not list,
|
|
124
|
+
spec-brief missing or too old where a brief is named, or a hook's line it
|
|
125
|
+
cannot read; nothing where all is well, and exit 0 whatever it finds.
|
|
126
|
+
`init` writes the entry beside its two, the plugin's `hooks/hooks.json`
|
|
127
|
+
carries it, and `doctor` notes a settings file that guards writes and has
|
|
128
|
+
none
|
|
129
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md),
|
|
130
|
+
[ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
|
|
131
|
+
Upgrading: `init` writes its lines once, so run `spec-harness init
|
|
132
|
+
--write` again to add the entry to a `.claude/settings.json` an earlier
|
|
133
|
+
release wrote; a plugin brings it with this release. A script reading
|
|
134
|
+
`doctor --format json` finds `claudeCode.sessionStart`.
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
|
|
138
|
+
- **A named pipe where a settings file, `.mcp.json` or `.spec-harness.json`
|
|
139
|
+
is read** no longer keeps `doctor`, `init` or every command from coming
|
|
140
|
+
back: the first two are read as files that cannot be read as JSON, and
|
|
141
|
+
`.spec-harness.json` is `neither a file nor a directory, not a
|
|
142
|
+
configuration file`, exit 2, as a directory there is.
|
|
143
|
+
- **`escalate` and `rule` say a path under git's directory, where the
|
|
144
|
+
harness keeps its state, that is no directory**, exit 2, in a line, where
|
|
145
|
+
they ended on Node's ENOTDIR and a stack.
|
|
146
|
+
- **A `core.hooksPath` that names a file is said in git's own words**, where
|
|
147
|
+
`doctor` and `init` gave the reason for a git older than 2.31.
|
|
148
|
+
- **`doctor`'s first line advises an array that unloads the project's
|
|
149
|
+
settings** before `init --write`, which writes no hook while it stands
|
|
150
|
+
and only advised the array in turn. `--format json` gains
|
|
151
|
+
`claudeCode.unloaded`.
|
|
152
|
+
|
|
153
|
+
### Documentation
|
|
154
|
+
|
|
155
|
+
- **Worktrees, the branches an app makes, and sessions that write through a
|
|
156
|
+
shell.** Two sections of the README, under *A round*. One says which
|
|
157
|
+
branch names the default templates read (`claude/brief-042-rotate-tokens`
|
|
158
|
+
is brief 042; `*` does not cross `/`; `claude --worktree <name>` makes
|
|
159
|
+
`worktree-<name>`, which needs a template of its own), and how a worktree
|
|
160
|
+
is checked: by its own branch and briefs, with the project's install where
|
|
161
|
+
it has none, at the commit as before the write; and that where a session's
|
|
162
|
+
project is a worktree with no install, the hook cannot start and the write
|
|
163
|
+
goes ahead. The other has, layer by layer, what the guard, the check as a
|
|
164
|
+
session starts, git's pre-commit hook, the audit and spec-brief's archive
|
|
165
|
+
each catch and let through, each run, and what every layer lets through:
|
|
166
|
+
an edit of the brief, a shell's write until the commit, `git commit
|
|
167
|
+
--no-verify`, and a Claude Code mod that approves a call a hook blocked.
|
|
168
|
+
It says why a shell `PostToolUse` hook and `"protected": "ask"` were not
|
|
169
|
+
built (ADR-0005, ADR-0006).
|
|
170
|
+
Upgrading: nothing to change; it describes this release.
|
|
171
|
+
|
|
172
|
+
## 0.18.0
|
|
173
|
+
|
|
174
|
+
`doctor` and `init` say more of what looks like a guard and checks
|
|
175
|
+
nothing: a matcher the Claude Code on this machine reads otherwise, and a
|
|
176
|
+
root on the hook's line that is in another work tree. And a directory where
|
|
177
|
+
git looks for its hook, or a probe for its report, ends in a sentence where
|
|
178
|
+
it ended on a stack
|
|
179
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
180
|
+
The hook loads only what it runs and opens its work tree in one start of
|
|
181
|
+
git, so it answers a write sooner: on a Windows workstation 1.0 seconds
|
|
182
|
+
where it took 1.4 with no brief named, and 1.9 where it took 3.6 before a
|
|
183
|
+
write to a protected file. Nothing any command prints changes
|
|
184
|
+
([ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
185
|
+
|
|
186
|
+
### Changed
|
|
187
|
+
|
|
188
|
+
- **A matcher that lists names with a comma is read as the Claude Code on
|
|
189
|
+
`PATH` reads it.** A comma separates names from Claude Code 2.1.191 on;
|
|
190
|
+
before it, by Claude Code's page on debugging a configuration, the matcher
|
|
191
|
+
matched no tool, and `doctor` said a line under `Edit, Write` guards
|
|
192
|
+
where Claude Code never started it. Under an older release the line is
|
|
193
|
+
now said as no guard, with the release and what the matcher is from
|
|
194
|
+
2.1.191 on, and `init --write` writes its own hooks beside it; where the
|
|
195
|
+
release cannot be told, or the matcher holds a `|` as well, it is
|
|
196
|
+
*cannot tell*.
|
|
197
|
+
Upgrading: under a Claude Code older than 2.1.191 `doctor` reports no
|
|
198
|
+
guard for such a line, exits 0 where it exited 1 for the plugin beside it,
|
|
199
|
+
and `--strict` fails a matcher with a comma and a `|`. Write the matcher
|
|
200
|
+
with `|` alone, as `Edit|Write`, or update Claude Code.
|
|
201
|
+
- **`doctor` fails where an array for a matcher keeps Claude Code from
|
|
202
|
+
loading a settings file's hooks.** By the same page, a `matcher` that is
|
|
203
|
+
an array is an invalid setting that starts no hook, and under
|
|
204
|
+
`PreToolUse` or `PermissionRequest` none of that file's other hooks
|
|
205
|
+
load either. Every line of the hook in such a file is said as one Claude
|
|
206
|
+
Code does not start, a guard that is off where it stood to be started for
|
|
207
|
+
a write, and `init` writes no hook into the file (`advise`).
|
|
208
|
+
Upgrading: `doctor` exits 1 for such a file, where it exited 0, or 1
|
|
209
|
+
under `--strict` alone: write the matcher as one string, its names
|
|
210
|
+
between `|`.
|
|
211
|
+
- **`doctor` fails for a hook's line whose root is in a work tree that does
|
|
212
|
+
not hold the project.** The hook opens the work tree its root is in and
|
|
213
|
+
passes a write to any file outside it with no word, so under a root in a
|
|
214
|
+
repository beside the project, or in one inside it, a submodule or a
|
|
215
|
+
linked worktree among them, no write to the project is checked: the line
|
|
216
|
+
is said as a guard that is off, and `init` advises. Under a root in a
|
|
217
|
+
work tree that holds the project, `doctor` says whose briefs a write
|
|
218
|
+
here is checked by, and fails nothing.
|
|
219
|
+
Upgrading: `doctor` exits 1 for such a line, where it exited 0: give the
|
|
220
|
+
line a directory in the project's work tree, or take `--root` off it.
|
|
221
|
+
- **`doctor` and `init` say a directory at the path of git's pre-commit
|
|
222
|
+
hook, where they ended on a stack.** git tries to run whatever it may
|
|
223
|
+
execute there, cannot run a directory, and stops every commit, so
|
|
224
|
+
`doctor` says the path and what git does with it and exits 1; `init`
|
|
225
|
+
advises and writes nothing over it. Where git may not execute it and
|
|
226
|
+
skips it, that is said and fails nothing. `--format json` has the two as
|
|
227
|
+
the `gitHook.state` `stops` and `ignored`.
|
|
228
|
+
Upgrading: `doctor` exits 1 there, and `init` 0, where each exited 2
|
|
229
|
+
with `unexpected error: Error: EISDIR`. Take the directory away, then run
|
|
230
|
+
`spec-harness init --git-hook --write`.
|
|
231
|
+
|
|
232
|
+
### Fixed
|
|
233
|
+
|
|
234
|
+
- **`probe` says a report that is a directory, where it ended on a
|
|
235
|
+
stack.** Where a probe's `junit` names a directory the repository holds,
|
|
236
|
+
the run is `no-report`, `cannot be read: it is a directory, not a
|
|
237
|
+
report`, and the probe invalid, exit 1, where it was `unexpected error`
|
|
238
|
+
and exit 2.
|
|
239
|
+
- **A command loads what it runs, and the hook no other command.** `hook
|
|
240
|
+
claude` is started before and after every write an agent makes, and the
|
|
241
|
+
command line loaded every command before it read which one was typed: 52
|
|
242
|
+
modules, 42 ms on Linux, close to half of a hook that names no brief. A
|
|
243
|
+
module that only some commands run is now loaded where one of them is
|
|
244
|
+
the command, and such a hook loads 26. Measured on Linux with the build
|
|
245
|
+
before this change and this one taking turns, at the median of ten runs,
|
|
246
|
+
the hook took 64 ms where it took 91, and 155 where it took 176 on a
|
|
247
|
+
brief's branch; `doctor` 81 where it took 99, and `--version` 40 where it
|
|
248
|
+
took 71. Nothing any command prints changes, nor an exit code, but in a
|
|
249
|
+
build with a file of `dist/` missing: a command that does not run that
|
|
250
|
+
module now answers, where every command ended on `build output is
|
|
251
|
+
missing`, and one that runs it ends on that line still, exit 2. In the
|
|
252
|
+
programmatic API, `checkPaths` takes as its reader a reader or a function
|
|
253
|
+
that makes one
|
|
254
|
+
([ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
255
|
+
- **A work tree is opened in two starts of git, asked at once, where it
|
|
256
|
+
took three in two waves.** Git is asked for the top of the work tree and
|
|
257
|
+
its common directory in one start, and where the command runs at the top
|
|
258
|
+
of its work tree, for the branch beside them. A process is what a command
|
|
259
|
+
takes on Windows. Measured on a workstation at rest, where starting node
|
|
260
|
+
to do nothing took 0.3 seconds, with the two builds taking turns, at the
|
|
261
|
+
median of ten runs: a hook that names no brief took 1.0 second where it
|
|
262
|
+
took 1.4, with this and the change above, and with every processor held
|
|
263
|
+
by other work, 13.7 seconds where it took 22.7. On Linux, where git
|
|
264
|
+
answers in milliseconds, it is three of the 26 milliseconds the hook is
|
|
265
|
+
quicker by. Below the top of a work tree, and in a directory that is in
|
|
266
|
+
none, as many processes are started as before.
|
|
267
|
+
- **A write the brief protects is refused without asking git for the
|
|
268
|
+
base.** The base is read for the allowed signers a ruling is checked
|
|
269
|
+
against, and it was resolved, three of git's processes, before the
|
|
270
|
+
brief's rulings were read. Where no ruling of the brief allows anything
|
|
271
|
+
the refusal now starts three processes where it started seven, and on
|
|
272
|
+
that workstation at rest took 1.9 seconds where it took 3.6; where one
|
|
273
|
+
does, the base is asked for as before.
|
|
274
|
+
|
|
12
275
|
## 0.17.0
|
|
13
276
|
|
|
14
277
|
`doctor` and `init` see four more ways a guard is set up and checks
|
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ The first of these names the round's brief; it is never guessed ([ADR-0004](http
|
|
|
59
59
|
2. `SPEC_BRIEF`;
|
|
60
60
|
3. the branch name: `brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}`.
|
|
61
61
|
|
|
62
|
+
The branch is read as HEAD names it, `refs/heads/` taken off, so a tag of the same name does not hide it: `claude/brief-042-x` beside a tag `claude/brief-042-x` names brief 042, where git's short name for it, `heads/claude/brief-042-x`, named none.
|
|
63
|
+
|
|
62
64
|
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:
|
|
63
65
|
|
|
64
66
|
| Variable | Set by |
|
|
@@ -77,6 +79,43 @@ A name that names no brief is not passed over for the next ([ADR-0004](https://g
|
|
|
77
79
|
- An id spec-brief does not know, or one it has archived, is not passed over either, whichever of the three names it: a command that acts on the brief stops with exit 2, and `doctor` says so in its report. The line says what named it: `the flag names brief 999, and spec-brief knows no such brief`, or `the environment`, `the branch`, or `the argument` for `context 999`.
|
|
78
80
|
- `context`, `rulings`, `audit` and `probe` take the brief's id as their argument as well, which comes before all three. The argument beside `--brief` is refused, whatever the two hold: one of them would not be read.
|
|
79
81
|
|
|
82
|
+
### Worktrees, and the branches an app makes
|
|
83
|
+
|
|
84
|
+
An app that makes a branch for a session puts a name of its own before the brief's, and the default `*/brief-{id}` reads it: `claude/brief-042-rotate-tokens` is brief 042. A `*` stands for a run of characters other than `/`, so `claude/team/brief-042-x` names no brief under the defaults, and does with a template of one star more. `branches` replaces the defaults, so name them beside it:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "branches": ["brief/{id}", "brief-{id}", "*/brief/{id}", "*/brief-{id}", "*/*/brief-{id}"] }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `claude --worktree <name>` makes its worktree under `.claude/worktrees/<name>/`, on a new branch named `worktree-<name>` ([Claude Code's worktrees page](https://code.claude.com/docs/en/worktrees), *Start Claude in a worktree*). No default reads it: `worktree-brief-042-x` names no brief. With `worktree-brief-{id}` among the `branches`, `claude --worktree brief-042-x` works on brief 042.
|
|
91
|
+
- A branch that names an id spec-brief does not list, as `claude/brief-999-x`, is not passed over for the next: every write of the session is held until the brief or the branch is mended. Where Claude Code starts the hook as a session starts, the entry `init` and the plugin write under `SessionStart`, the session is told so before its first write.
|
|
92
|
+
- A tag of the same name as the branch does not hide it: the branch is read as HEAD names it.
|
|
93
|
+
- In a worktree session started from the desktop app, and after Claude enters a worktree mid-session, Claude Code starts the hook with `${CLAUDE_PROJECT_DIR}` at the main checkout and hands it the worktree as the question's `cwd` (the worktrees page, *What worktrees share with the main checkout*; [the hooks reference](https://code.claude.com/docs/en/hooks), *Reference scripts by path*). The worktree's branch names the brief, and a worktree with no install of its own is checked with the project's: an Edit of a file brief 042 protects, in a worktree on `claude/brief-042-x` of a main checkout on `main`, is refused.
|
|
94
|
+
- git's pre-commit hook in such a worktree runs the main work tree's install where the worktree has none, and refuses the same file at the commit. A hook an earlier release wrote reads the worktree's own install alone: take it away and run `init --git-hook --write`.
|
|
95
|
+
- Where `${CLAUDE_PROJECT_DIR}` is the worktree itself, the project root a session started in, the hook's script is looked for in the worktree's own `node_modules`. A worktree is a fresh checkout (the worktrees page, *Set up the worktree environment*), and where it has no install, node cannot find the script and exits 1: Claude Code reads that as a hook's non-blocking error, and the write goes ahead (the hooks reference, *Exit code output*). Install in the worktree before such a session writes. From Claude Code 2.1.295, `"onFailure": "block"` on the hook's entry holds the write instead; neither `init` nor the plugin writes it ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
96
|
+
|
|
97
|
+
### Sessions that write through a shell, with no rulings
|
|
98
|
+
|
|
99
|
+
Claude Code starts the guard for `Edit`, `Write`, `MultiEdit` and `NotebookEdit`, the matcher `init` and the plugin write. A `Bash` or `PowerShell` call takes a command, not a file, and the guard is not asked about what it writes. With brief 042 covering `src/**` and protecting `src/db/schema.ts`, and `notes.md` outside its scope, each layer was run:
|
|
100
|
+
|
|
101
|
+
| Layer | Catches | Lets through |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| The guard, before an Edit | an Edit of `src/db/schema.ts`: refused, with the next step | an Edit of `notes.md` under `outOfScope: "warn"`; every shell command |
|
|
104
|
+
| The guard, after an Edit | an Edit of `notes.md`: a warning to Claude | every shell command |
|
|
105
|
+
| The hook as a session starts | a configuration that does not load, an id spec-brief does not list, spec-brief missing or too old where a brief is named: one line, to the person and to Claude | everything: it holds nothing, and the event cannot block |
|
|
106
|
+
| git's pre-commit hook, `init --git-hook` | every staged path, a shell's writes among them: `src/db/schema.ts` refused; `notes.md` warned of under `warn`, refused under `ask` and `deny` | `git commit --no-verify`, which skips the hook ([githooks](https://git-scm.com/docs/githooks), *pre-commit*) |
|
|
107
|
+
| `spec-harness audit` | the round's commits from the base: `src/db/schema.ts` changed, an error, and `notes.md`, a warning; exit 1 | what the brief, as it stands at the end, allows |
|
|
108
|
+
| `spec-brief archive --strict --base <branch>` | the same, both errors: the brief is not archived | the same |
|
|
109
|
+
|
|
110
|
+
What every layer lets through:
|
|
111
|
+
|
|
112
|
+
- **An edit of the brief.** Its file is always writable, and the audit and the archive read the brief as it stands at the end: a round that adds `notes.md` to `affectedFiles` and takes `src/db/schema.ts` out of `protectedFiles` is archived with `--strict`. The change is in the brief's diff, for the person who approved it to read.
|
|
113
|
+
- **A shell's write**, until the commit.
|
|
114
|
+
- **`git commit --no-verify`**, until the audit.
|
|
115
|
+
- **A Claude Code mod** that handles `tool.check` can approve a call a `PreToolUse` hook blocked, unless the hook is in managed settings ([Claude Code's permissions page](https://code.claude.com/docs/en/permissions), *Precedence among rules, hooks, and mods*).
|
|
116
|
+
|
|
117
|
+
Two more layers were weighed and not built. A `PostToolUse` hook for the shell tools: every shell command would pay for it, it is easy to get around, and the audit stays the gate ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md), amended 2026-09-30). And `"protected": "ask"`: a protected file passes the guard only by a ruling whose commit a person signed, which the audit and the archive check again ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)), and an approval given in the session is kept nowhere they read.
|
|
118
|
+
|
|
80
119
|
## Commands
|
|
81
120
|
|
|
82
121
|
In the order a round meets them; `init` and `doctor` set a repository up and check it.
|
|
@@ -132,10 +171,13 @@ May the round write this?
|
|
|
132
171
|
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.
|
|
133
172
|
|
|
134
173
|
- `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. When it cannot answer - the configuration does not load, the briefs cannot be read, the harness meets an error it did not expect - it exits 2, and before a write Claude Code then holds the write.
|
|
174
|
+
- In a worktree, a write is decided by the worktree's brief. Claude Code names the project a session started in, and hands the hook the directory Claude works in as the question's `cwd`: in a worktree session started from the desktop app, and after Claude enters a worktree, the first is the main checkout and the second the worktree. Where `cwd` is in another work tree of the same repository, that work tree's branch names the brief, its briefs and configuration are read, and its siblings are looked for in its own `node_modules` and then the project's, so a worktree with no install is checked with the project's. A path it does not hold, and every path where `cwd` is in another repository or in none, is decided by the project as before ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
175
|
+
- After a write the hook answers from what it decided before it, for the same call: the run before keeps the warnings it would give under the git common directory, in `spec-harness/calls/`, named by the question's `session_id` and `tool_use_id`, and the run after takes them away and answers with no process started. A refused call keeps nothing, nor one that writes `.spec-harness.json`, spec-brief's configuration, the brief or a file beside it, which are decided again after the write; what no run after a write reads is removed after a day. Under `outOfScope: "ask"` or `"deny"` no write is warned of, and the run after one says nothing with no process started ([ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
176
|
+
- As a session starts, resumes, is cleared, compacted or forked, `hook claude` answers Claude Code's SessionStart with what would hold every write of the session, in one line, to the person as `systemMessage` and to Claude as `additionalContext`: a configuration that does not load, a brief the line, `SPEC_BRIEF` or the branch names that spec-brief does not list, spec-brief missing or too old where a brief is named, and what is wrong with the hook's own line. Nothing is said where all is well, and it exits 0 whatever it finds: the event cannot block, and the session's start is not to be stopped by it ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
135
177
|
- `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`.
|
|
136
178
|
- `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.
|
|
137
179
|
- 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.
|
|
180
|
+
- 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, two values for an option that takes one, `--help` beside `--version` - 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.
|
|
139
181
|
|
|
140
182
|
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)).
|
|
141
183
|
|
|
@@ -156,6 +198,8 @@ git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
|
|
|
156
198
|
|
|
157
199
|
A memo is what a person rules on, so none is written with a part that says nothing: a path, the reason, a recommendation or an option that is empty is [refused](#an-input-that-names-nothing), by `escalate` and by the server's `request_escalation` alike. An option is `"<label>: <cost>"`: the label names the choice and may not be empty, and the cost may be left out, the memo then giving the choice alone.
|
|
158
200
|
|
|
201
|
+
Requests wait under git's directory, in `spec-harness/escalations` (ADR-0003). Where something that is no directory stands at either path, `escalate` and `rule` say which, exit 2, in a line: `.git/spec-harness is not a directory, where spec-harness keeps its state in one: move it away, and run the command again`.
|
|
202
|
+
|
|
159
203
|
`escalate --list` prints every request that waits, whichever round raised it, and `escalate --show <id>` one memo. Neither raises a request, so neither reads `--path`, `--reason`, `--option`, `--recommend` or `--brief`, and either beside one of them is [refused](#an-input-that-names-nothing): the request was never raised, and the list looked like an answer to it.
|
|
160
204
|
|
|
161
205
|
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.
|
|
@@ -340,18 +384,21 @@ Configures the family to agree. It prints the plan, merges into files that exist
|
|
|
340
384
|
- 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;
|
|
341
385
|
- when it cannot tell, it says so and writes none: set `"base"` yourself.
|
|
342
386
|
- **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.
|
|
387
|
+
- Beside the hook's entries under `PreToolUse` and `PostToolUse`, one under `SessionStart`, with no matcher: every way a session starts. In a file whose two an earlier release wrote, `init --write` adds it by itself; beside a line of the hook a person wrote or changed it adds none.
|
|
343
388
|
- 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`).
|
|
344
389
|
- 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.
|
|
345
390
|
- 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. So where the hook stands in a settings file of your own, which `init` does not write.
|
|
346
391
|
- Where the plugin that is turned on is not on this machine, as [`doctor`](#doctor) reads it, `init` still writes neither entry: a plugin that is there after all would bring them a second time. It says what `doctor` says (`advise`), and how to have the entries in the files instead. Where that cannot be told, it says so beside the `skip`.
|
|
347
392
|
- `"disableAllHooks": true` in a settings file is said of that file (`advise`): the hooks `init` writes do not run while it stands. `init` writes what it would have written.
|
|
348
393
|
- A line of the hook that is there already is known as [`doctor`](#doctor) knows it: by its command and the words `hook claude`, however a person changed it, with the event and the matcher its entry stands under. Where the file holds such a line that Claude Code starts for a write, `init` writes no hook into it, since a second line would run beside the first, and says which line it left, in `doctor`'s words for it (`keep`, or `advise` where the line is no guard: it turns the guard off, holds every write, or is waited on before none). It rewrites no line but the one 0.1 wrote, and completes only its own line where that stands under one event alone. A line Claude Code starts for no write is left as it is, and `init` writes its own beside it.
|
|
394
|
+
- A matcher is read as [`doctor`](#doctor) reads it, for the Claude Code on `PATH`. A line under names with a comma, `Edit, Write`, is one a Claude Code older than 2.1.191 starts for no write, so there `init` writes its own hooks beside it; it asks Claude Code its release only where what it reads of a line turns on it. And into a file that holds an array for a matcher under `PreToolUse` or `PermissionRequest` it writes no hook (`advise`): Claude Code loads none of that file's hooks, so write the matcher as one string, its names between `|`, and run `init` again.
|
|
349
395
|
- **git's pre-commit hook**, where git runs it from: `core.hooksPath`, relative, absolute or under `~`, and the shared hooks of a linked worktree.
|
|
350
396
|
- It refuses a commit that changes what the active brief protects, for any agent or none, a shell's writes included.
|
|
351
397
|
- With `--git-hook` it is written; without, the plan advises it (`advise`).
|
|
352
|
-
- It runs `node` and the script in the project's `node_modules`, as the Claude Code hooks do, and no `npx`. git starts a pre-commit hook at the top of the work tree it commits in, so the script is read from there: a linked worktree is guarded by its own install, and
|
|
398
|
+
- It runs `node` and the script in the project's `node_modules`, as the Claude Code hooks do, and no `npx`. git starts a pre-commit hook at the top of the work tree it commits in, so the script is read from there: a linked worktree is guarded by its own install, or, where it has none, by the main work tree's, by its own branch and briefs; each repository that shares a hooks directory is guarded by its own. Where neither has the install, the hook says so and the commit waits. A hook an earlier release wrote reads the work tree's own install alone: take it away and run `init --git-hook --write` for this one.
|
|
353
399
|
- It is a POSIX `sh` script on every platform: Git for Windows runs a hook with the `sh` it ships.
|
|
354
400
|
- A hook that exists is never replaced: `init` says to add the line to it. One that runs spec-harness through `npx`, as `init` wrote it through 0.9.1, keeps working, and `init` names the line that runs it with `node`.
|
|
401
|
+
- Nor is anything else that stands at the hook's path. A directory there is said as [`doctor`](#doctor) says it (`advise`), with `--git-hook` or without, and nothing is written over it.
|
|
355
402
|
- **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`).
|
|
356
403
|
- 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:
|
|
357
404
|
- GitHub: CODEOWNERS and a protected branch;
|
|
@@ -365,6 +412,7 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
365
412
|
- **The siblings**: which are installed, at which versions, and how each is run.
|
|
366
413
|
- A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1.
|
|
367
414
|
- A command named under `tools` is run as named, and its version is not checked.
|
|
415
|
+
- In a linked worktree with no install of its own, the siblings are the main work tree's, as git's hook runs them there, and each such row says whose install it is: `(0.8.0, the install of /work/app: this work tree has none of its own)`. `--format json` has it as `elsewhere`, `null` for a sibling of the work tree's own.
|
|
368
416
|
- **The repository**: its root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names.
|
|
369
417
|
- A brief no round can work under is said on that line, with why: `brief none: the flag names brief 999, and spec-brief knows no such brief`. So is one spec-brief has archived, an empty `--brief`, and a spec-brief that could not list the briefs. The guard holds every write while it stands, which is often why `doctor` is being run.
|
|
370
418
|
- It fails nothing, as a base that names no commit fails nothing: `doctor` exits as it would have. `--format json` has the id as `brief` and the reason as `briefProblem`, `null` where there is none.
|
|
@@ -377,18 +425,23 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
377
425
|
- A `cert-authority` line is a certificate authority's key, not a person's, and is left out.
|
|
378
426
|
- 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.
|
|
379
427
|
- **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.
|
|
428
|
+
- Where a settings file guards writes and none starts the hook as a session starts, a note says so and how to add it, `spec-harness init --write`, and fails nothing. `--format json` has `claudeCode.sessionStart`.
|
|
380
429
|
- `"disableAllHooks": true` in a settings file switches the guard off: by [Claude Code's settings reference](https://code.claude.com/docs/en/settings-reference) it turns off the hooks of the user's, the project's and the local settings and of plugins. `doctor` reads the key from the three files as Claude Code merges them, the last that sets it deciding, names the file that holds it, and exits 1 where there are guard hooks for it to switch off. Where nothing is wired it is a note. Hooks of managed settings, and of a plugin managed settings force on, keep running; `doctor` reads no managed settings and no `--settings`.
|
|
381
430
|
- 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.
|
|
382
431
|
- Only an entry Claude Code runs as a command is a line of the hook, `"type": "command"`. An entry of another handler type (`http`, `mcp_tool`, `prompt`, `agent`) runs no command, whatever it holds, and is not reported. One with no `type`, or with a word the hooks reference does not list, is *cannot tell*, never fine: the reference requires the field and says nothing of an entry without it; `--strict` fails it.
|
|
383
432
|
- 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`.
|
|
384
433
|
- 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.
|
|
385
434
|
- A `--root` that is a directory outside any git work tree is a guard that is off as well, and the one fault the hook says nothing of: it reads a session outside a work tree there, checks no write, and exits 0. `doctor` asks git about the root as the hook does, says so, and exits 1: `"hook claude --root /elsewhere" in .claude/settings.json: --root is "/elsewhere", a directory outside any git work tree, which the hook reads as a session outside one, exit 0 and no word, so the guard is off and no write is checked`.
|
|
435
|
+
- A `--root` in another git work tree than the project's is read by where that work tree is. The hook opens the work tree its root is in and decides by that work tree's briefs, and a file outside it is none of its repository's, which the hook passes with no word. So where that work tree does not hold the project - another repository beside it, or one inside it: a repository of its own, a submodule, a linked worktree - no write to a file of the project is checked. The guard is off, and `doctor` exits 1: `"hook claude --root ../other" in .claude/settings.json: --root is "../other", a directory in /work/other, the work tree of another repository, which does not hold this project: the hook checks a write to a file of that work tree, by its briefs, and passes any other with no word, exit 0, so the guard is off and no write to a file of this project is checked`. Where the work tree holds the project, as a repository the project is nested in does, or the main work tree around a linked one, a write here is checked, by that work tree's briefs and its branch and under the path the file has there: `doctor` says whose briefs they are and fails nothing. A work tree of the project's own repository, linked or main, is named `another work tree of this repository`, and is read by the same rule: a linked worktree beside the project holds none of its files.
|
|
386
436
|
- A line that names a brief which is not one, as `--brief 404` does where spec-brief knows no brief 404 or has archived it, holds every write: the hook exits 2 before each until the line is mended. `doctor` asks spec-brief about the name as the hook does, in the work tree the line's root is in, says it in the words the hook holds the write with, and exits 1: `"hook claude --brief 404" in .claude/settings.json: the flag names brief 404, and spec-brief knows no such brief, so the hook holds every write until the line is mended: name a brief spec-brief lists, or take --brief off the line`.
|
|
387
437
|
- An entry is read with the event it stands under and its matcher, as [Claude Code's hooks reference](https://code.claude.com/docs/en/hooks) has them: Claude Code waits on a hook before a tool call under `PreToolUse` alone, and starts an entry for the tools its matcher lets through - every tool where the matcher is left out, empty or `*`, the tools it names where it is a list of names, and the names a pattern matches otherwise. An entry that runs in the background (`async`, `asyncRewake`) is waited on for none.
|
|
388
438
|
- A line Claude Code waits on before no write is not the guard, whatever the hook would do with it. `doctor` says so with what the line stands under, `"hook claude" in .claude/settings.json: Claude Code waits on it before no write, so it is not the guard and refuses none (under PreToolUse it has the matcher "Bash")`, and a file whose only line is started for no write, before one or after, is not counted among `init`'s entries. It fails nothing: no guard is a choice.
|
|
389
439
|
- A line waited on before some of `Edit`, `Write` and `NotebookEdit` guards those, and `doctor` names the rest, whose writes go unchecked. A line started after no write warns of none, which is said too. Neither fails anything.
|
|
390
440
|
- Those three are the tools a Claude Code from 2.1.139 on writes a file with, by its [tools reference](https://code.claude.com/docs/en/tools-reference). The hook also reads the file a `MultiEdit` names, a tool no release from 2.0.0 on declares, and `init`'s matcher keeps the name; `doctor` says of no line that it leaves `MultiEdit` unchecked, and a line under `Edit|Write|NotebookEdit` is said with nothing more.
|
|
391
|
-
- An `if` on the entry has Claude Code start it only for the tool calls its rule matches. The rule is not read, so which writes the line is asked on is *cannot tell*, never fine; `--strict` fails it. So is a matcher that is
|
|
441
|
+
- An `if` on the entry has Claude Code start it only for the tool calls its rule matches. The rule is not read, so which writes the line is asked on is *cannot tell*, never fine; `--strict` fails it. So is a matcher that is neither text nor an array, or is no pattern, or is a pattern that takes more than a tenth of a second to test against the three names: one can be written that never finishes, and `doctor` and `init` come back all the same.
|
|
442
|
+
- A matcher that lists names with a `,` is read as the Claude Code on `PATH` reads it. By [Claude Code's page on debugging a configuration](https://code.claude.com/docs/en/debug-your-config), a comma separates names as a `|` does from 2.1.191 on; before it, a comma was read as part of a pattern, and the matcher matched no tool. So under 2.1.139 to 2.1.190 a line under `Edit, Write` is waited on before no write and is not the guard: `doctor` says it with the release and what the matcher is from 2.1.191 on, `(under PreToolUse it has the matcher "Edit, Write", which Claude Code 2.1.190 reads as a pattern that matches no tool: a comma separates names from 2.1.191 on, where the matcher is Edit and Write)`, and fails nothing for it, as for any line that is no guard. Where the release cannot be told, which writes the line is started for is *cannot tell*, never fine; `--strict` fails it. So is a matcher with a `,` and a `|` under an older release, where read as a pattern it would match a tool, as `Edit|Write, NotebookEdit` would `Edit`: the page has such a matcher never match, and has the comma read as part of a pattern, and for that matcher the two disagree. A matcher with no comma, the one `init` writes among them, is read the same under every release.
|
|
443
|
+
- A matcher of names with a hyphen in one, `Edit|Write|my-tool`, is read as the Claude Code on `PATH` reads it too. By [Claude Code's changelog](https://code.claude.com/docs/en/changelog), 2.1.195, such a matcher matched as a substring before that release and exactly from it on; the changelog does not say how the rest of it was read, the names it lists exactly, the matcher as a pattern, under which `Edit` is in `NotebookEdit` too, or its whole text, which is in no tool's name. So under 2.1.139 to 2.1.194, and where the release cannot be told, which writes the line is started for is *cannot tell* wherever those readings differ, never fine; `--strict` fails it: `(under PreToolUse it has the matcher "Edit|Write|my-tool", which Claude Code 2.1.194 may read otherwise: before 2.1.195 a matcher with a hyphenated name in it matched as a substring, and Claude Code's changelog does not say how the rest of it was read; from 2.1.195 on it is Edit and Write)`. A matcher whose readings all come to no tool that writes, `Bash|my-tool`, is no guard under any release, and nothing is said of the release.
|
|
444
|
+
- A matcher that is an array, `["Edit", "Write"]`, is by the same page an entry Claude Code lists as an invalid setting, and no hook under it is started. Where one stands under `PreToolUse` or `PermissionRequest`, in an entry of the hook's or of any other program's, Claude Code loads none of that settings file's other hooks either. `doctor` says so of every line of the hook the file holds, and a line that stood to be started for a write is a guard that is off, exit 1: `"hook claude" in .claude/settings.json: .claude/settings.json has an array for a matcher under PreToolUse, ["Bash"], which Claude Code lists as an invalid setting, and beside one under PreToolUse or PermissionRequest it loads none of the file's other hooks, so the guard is off and no write is checked`. The hooks of another settings file are read as that file has them. Where such an array stands in the project's `.claude/settings.json`, the file `init` writes its hooks into, `doctor`'s advice is the array's first, since `init` writes no hook there while it stands, and then `init --write`. `--format json` has the array as `claudeCode.unloaded`, `null` where there is none.
|
|
392
445
|
- 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.
|
|
393
446
|
- `"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.
|
|
394
447
|
- `--format json` has each line under `claudeCode.lines`: its `file`, `events`, the `line` as written, its `state` (`guards`, `off`, `held`, `unstarted` or `unknown`), `onFailure`, the writing tools Claude Code waits on it `before` a write by and starts it `after` one by, of `Edit`, `Write` and `NotebookEdit` (`null` where that cannot be told), and what is said of it as `detail`, `cover` and `note`, `null` where there is nothing to say. Beside the lines, `claudeCode.presence` is whether the plugin is on this machine, `null` where no plugin is on and otherwise its `state` (`present`, `absent` or `unknown`) with the reason as `detail`; `claudeCode.hooksOff` is the settings file whose `disableAllHooks` is `true`, or `null`; and `claudeCode.notes` holds what is said of the two.
|
|
@@ -403,6 +456,10 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
403
456
|
- 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`.
|
|
404
457
|
- **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.
|
|
405
458
|
- A hook that runs it through `npx` is one that runs it. A note names the line that runs it with `node`: `npx` starts npm to start node on every commit, and under npm 12 prints two `npm notice run` lines each time. A note, never a failure; `--format json` has it as `gitHook.note`.
|
|
459
|
+
- What stands at the hook's path and is no file is no hook, and git takes it for one all the same. A directory there, which a repository whose `core.hooksPath` is in its work tree can hold, is one git tries to run where it may execute it, which on Windows is always: it cannot, and stops every commit, with `cannot spawn` on Windows and `cannot exec` elsewhere. `doctor` says the path, what it is and what git does with it, and exits 1, since nothing is checked there and nothing is committed: `.git/hooks/pre-commit is a directory, not a hook, and git stops every commit at it`. Where git may not execute it, it skips it as it skips a hook that is not executable: that is said, and fails nothing. `--format json` has the two as the `gitHook.state` `stops` and `ignored`. Only a file is read, so `doctor` and `init` come back from a named pipe there as well.
|
|
460
|
+
- A hook file the account may not read is said with what git does with it, where `doctor` and `init` ended on a stack. Where git may not execute it either, it skips it with a hint and commits: said, and nothing fails. Where it may, git runs it, and a script the shell cannot read stops every commit, where a program runs unread: whether it runs spec-harness is *cannot tell*, never fine; `--strict` fails it. `--format json` has the `gitHook.state` `unreadable`.
|
|
461
|
+
- A `core.hooksPath` that names a file is one git names no hook under: it says `Invalid path ...: Not a directory`, and commits with no hook. `doctor` says that where git runs its hooks cannot be told, in git's own words, where it gave the reason it has for a git older than 2.31.
|
|
462
|
+
- **What stands where a file is read.** A settings file, `.mcp.json` or a record of Claude Code's plugins that is neither a file nor a directory, such as a named pipe, is read as one that cannot be read as JSON, as a directory is: a pipe nothing writes to was read without end, and neither `doctor` nor `init` came back. `.spec-harness.json` that is neither stops every command, the hook among them, with `.spec-harness.json is neither a file nor a directory, not a configuration file`, exit 2, as one that is a directory does.
|
|
406
463
|
|
|
407
464
|
### An input that names nothing
|
|
408
465
|
|
|
@@ -428,16 +485,17 @@ An option, an argument or a value that is given and names nothing is refused: ex
|
|
|
428
485
|
- **An argument a command does not take.** `context`, `rulings`, `audit` and `probe` take one brief's id at most, `rule` one escalation's id, `guard` its paths and `hook` the word `claude` or `git`; `escalate`, `premises`, `init`, `mcp` and `doctor` take none. `init mydir` set up the directory it stood in; `init --root mydir` sets up another.
|
|
429
486
|
- **A value that names nothing.** An option that takes a value refuses one that is empty or only space, by the option's name: `--brief is "", which names no brief`. So does an argument, as in `guard ""`. No option gives an empty value a meaning: leave the option out for what the command does without it.
|
|
430
487
|
- **A `--root` that is no directory**, a file or a path that is not there: `--root is "nowhere", which is not a directory`.
|
|
488
|
+
- **An option that takes one value, given two.** `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001` was brief 001: the parser kept the last, and the earlier went unread with no word. It is `--root is given twice, as "nowhere" and as ".", and one of the two would go unread; give it one`. Two values are compared as the option reads them, so the same thing given twice is the option once: `--root .` beside `--root ./`, a directory however it is spelt from where the command is started, and `--brief 12` beside `--brief 012`, a brief by its id as spec-brief's names are matched. A base, a format, an id and text are compared as written. `--path` and `--option` are lists, and each use adds to the list.
|
|
431
489
|
- **A `.spec-harness.json` that is a directory** does not load, and is said so in a line, as one that is not JSON is.
|
|
432
490
|
|
|
433
|
-
`--help` and `--version`
|
|
491
|
+
A line that is wrong is refused with `--help` or `--version` on it, each of the above and a command that is none, as an option no command has always was: `doctor --help --root a --root b` printed the help, exit 0. `--help` beside `--version` asks for two answers, of which one would go unread, and is refused: `--help is given beside --version, and one of the two answers would go unread; give one of them`. On a line that is right, either answers, a command that needs more than the line gives it among them, as `guard --help` does. Three things are read another way, each for its reason:
|
|
434
492
|
|
|
435
493
|
- **`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)).
|
|
436
494
|
|
|
437
495
|
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.
|
|
438
496
|
|
|
439
497
|
- **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.
|
|
440
|
-
- **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
|
|
498
|
+
- **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; two values for an option that takes one, whichever option it is, as in `hook claude --brief 001 --brief 404`, with `--help` beside them or not; `--help` beside `--version`. 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.
|
|
441
499
|
|
|
442
500
|
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.
|
|
443
501
|
- **`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.
|
package/dist/cli.d.ts
CHANGED
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
* failed, an output its reader closed, an error nothing here expected. A run
|
|
9
9
|
* that could not look must never exit as though it looked and found nothing
|
|
10
10
|
* (spec-core ADR-0005).
|
|
11
|
+
*
|
|
12
|
+
* A command loads what it runs. This module imports what every run needs,
|
|
13
|
+
* the hook's before a write among them, and a module that only some
|
|
14
|
+
* commands run is imported where one of them is the command: the hook
|
|
15
|
+
* runs before and after every write an agent makes, and what it loaded of
|
|
16
|
+
* the other commands was a wait before each (ADR-0002). A module that
|
|
17
|
+
* cannot be loaded is thrown where it was asked for, and ends the run in a
|
|
18
|
+
* line and exit 2, as every error does.
|
|
11
19
|
*/
|
|
12
20
|
import { type CliIO } from './workspace.js';
|
|
13
21
|
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
|
|
22
|
+
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, PostToolUse or SessionStart hook\n (stdin). At session start it says what would hold every write.\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, its hook's line turns it off or holds every\n write, disableAllHooks or an array for a matcher switches it\n off, the plugin that is turned on is not on this machine and\n nothing else guards, Claude Code is too old to run its hooks,\n or git stops every commit at what stands for its hook.\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 One of the two: both on one line are refused.\n\nAn option a command does not read, an argument it does not take, an option or an\nargument given an empty value, a brief given both as the argument and as --brief,\nand an option that takes one value given two that name two things are refused, exit 2,\nnot read as if they were not there, and so with --help or --version on the line. The\nsame value twice, as --root . --root ./, is read once. doctor says an empty --brief or\n--base in its report. hook claude reads its line as every release has, and says a line\nit cannot read, two values for one option among them, and a --root that is not a\ndirectory, with exit 1: it refuses nothing for either.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
|
|
15
23
|
/**
|
|
16
24
|
* Runs the command line and resolves to the exit code, for an error nothing
|
|
17
25
|
* below expected too.
|
|
@@ -31,6 +39,12 @@ export declare const HELP = "spec-harness - context, guards, audits and rulings
|
|
|
31
39
|
* launcher answers in the same words; here it is a caller's stream that
|
|
32
40
|
* throws it. It is read by its code: stdout and stderr are the only pipes
|
|
33
41
|
* the harness writes to.
|
|
42
|
+
*
|
|
43
|
+
* Nor is a module of the build that is not there. The launcher says a build
|
|
44
|
+
* without its command line in a line, and a module a command asks for where
|
|
45
|
+
* it runs is said in the same line: a build with a file missing is not a
|
|
46
|
+
* defect a stack leads to. It is read by its code too: the harness imports
|
|
47
|
+
* nothing but its own modules and Node's.
|
|
34
48
|
*/
|
|
35
49
|
export declare function run(argv: readonly string[], io: CliIO): Promise<number>;
|
|
36
50
|
export declare function main(argv?: readonly string[]): Promise<number>;
|