@descent-vtt/spec-harness 0.11.1 → 0.13.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.
@@ -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.11.1"
13
+ "version": "0.13.0"
14
14
  }
15
15
  ]
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-harness",
3
- "version": "0.11.1",
3
+ "version": "0.13.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,165 @@ 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.13.0
13
+
14
+ `hook claude` says a `--root` on its line that is not a directory, with
15
+ exit 1 and no write refused, where the guard was off and nothing said so.
16
+ And four more inputs that named nothing, or were not read, are refused by
17
+ name: a brief `premises` is given that spec-brief does not know, an option
18
+ `escalate --list` or `--show` leaves unread, and a brief given as the
19
+ argument and as `--brief` at once.
20
+
21
+ ### Changed
22
+
23
+ - **`hook claude` says a `--root` on its line that is not a directory, and
24
+ exits 1.** It exited 0 with no word, as outside a git work tree, and the
25
+ write went on unguarded; it is now one line on stderr that names the root,
26
+ and exit 1, which Claude Code shows as a hook error without blocking the
27
+ write
28
+ ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
29
+ Upgrading: no release wrote a `--root` on the hook's line; if you added
30
+ one, point it at a directory that exists, or take it off.
31
+ - **`premises` refuses a brief `--brief` or `SPEC_BRIEF` names that
32
+ spec-brief does not know**, as every other command does: `premises
33
+ --brief 999` was read as no round, and held the round's own premise
34
+ against it. It is exit 2 and `the flag names brief 999, and spec-brief
35
+ knows no such brief`
36
+ ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
37
+ Upgrading: name a brief spec-brief lists, or none. A branch whose name
38
+ reads as a brief's and names none is read as no round, as before.
39
+ - **`escalate --list` and `--show` refuse an option they do not read.**
40
+ Beside either, `--path`, `--reason`, `--option`, `--recommend` and
41
+ `--brief` did nothing, and `--show` did nothing beside `--list`: the list
42
+ was printed, exit 0, and a request spelled out beside it was never raised
43
+ ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
44
+ Upgrading: give `--list` or `--show` with `--root` and `--format` alone,
45
+ and raise a request in a call of its own.
46
+ - **A brief given as the argument and as `--brief` at once is refused**,
47
+ whatever the two hold, by `context`, `rulings`, `audit` and `probe`:
48
+ `audit 012 --brief 013` audited 012 with no word about 013. Upgrading:
49
+ give one of them. `SPEC_BRIEF` and the branch beside either are read as
50
+ before.
51
+
52
+ ### Added
53
+
54
+ - `findActive`, from the package, takes the brief an `argument` names, which
55
+ comes before the flag, and gives `argument` as its `source`.
56
+
57
+ ### Fixed
58
+
59
+ - A brief that is not one is said by what named it: `the argument names
60
+ brief 999` for `context 999`, and `"brief" names brief 999` from the
61
+ server's tools, where both said `the flag`.
62
+ - The context packet ends once the sentence in which it quotes why
63
+ spec-guard could not be asked: a refusal spec-guard ends with a full stop
64
+ of its own, as each has since its 0.21.0, read `... in package.json..
65
+ Treat every ADR as binding`.
66
+
67
+ ## 0.12.0
68
+
69
+ A probe's command is answered when its shell ends, by the shell's exit code,
70
+ where one that left something holding its output was waited for until its
71
+ timeout and then called stopped. And an input that is set and names nothing
72
+ is refused by a line that names it, where it was read as if it had not been
73
+ given: an option another command reads, an argument a command does not
74
+ take, an empty `--root`, `--brief` or `--base`, an escalation with an empty
75
+ path or reason, from the command line or through the server. Every command
76
+ line a release's `init` wrote is read as it was.
77
+
78
+ ### Changed
79
+
80
+ - **A probe's command is answered when its shell ends, by the shell's exit
81
+ code.** A command that ended and left something holding its output, as a
82
+ server started in the background does, was waited for until its timeout,
83
+ ten minutes by default, and then counted as stopped: its `run` was a
84
+ `timeout` and the probe `invalid`, and its `setup` stopped the probe.
85
+ `probe` now waits three seconds for that output, as GitHub's runner waits
86
+ five for a step's, judges the command by its exit code and what it had
87
+ printed, and says that something it started was left running
88
+ ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
89
+ Upgrading: a command's work must be done when its shell ends, so have it
90
+ wait for what it starts. A probe that was `invalid` at its timeout may now
91
+ be `measured`, `vacuous`, `fixed` or `still-failing`, and a `setup` that
92
+ stopped the probe may pass.
93
+ - On Linux and macOS, what a probe's command left running in its process
94
+ group is stopped when `probe` is done at that commit, before the worktree
95
+ is removed. One that held the command's output was stopped at the
96
+ command's timeout, and one that held nothing ran on after `probe` had
97
+ ended. Upgrading: a process a `setup` starts in the background serves the
98
+ runs at that commit and does not outlive them. On Windows it still runs
99
+ on, since nothing a command left can be reached there.
100
+ - **An option a command does not read is refused by that command.** Every
101
+ command took every option, so `doctor --note x`, `init --base main`,
102
+ `mcp --brief 012` and `hook git --strict` ran as if the option were not
103
+ there. Each is now exit 2 and one line that names it, `doctor does not
104
+ take --note; its options are --brief, --base, --root, --format and
105
+ --strict`
106
+ ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md),
107
+ [An input that names nothing](README.md#an-input-that-names-nothing)).
108
+ Upgrading: drop the option. The README's table has what each command
109
+ reads; `--strict` is for `guard`, `audit`, `premises` and `doctor`, and
110
+ `init` and `mcp` take neither `--brief` nor `--base`.
111
+ - **An argument a command does not take is refused.** `doctor extra`,
112
+ `premises extra` and `context 012 extra` ran as if it were not there, and
113
+ `init mydir` set up the directory it stood in; each is exit 2 with the
114
+ argument named. Upgrading: drop the argument; `init --root mydir` sets up
115
+ another directory.
116
+ - **An option or an argument given an empty value is refused by name, not
117
+ read as left out.** `--root ""` ran in the current directory, `--brief ""`
118
+ was answered for the branch's brief, `audit --base ""` measured no change
119
+ and warned, and `guard src/a.ts ""` judged the repository itself. Each is
120
+ exit 2 and a line such as `--root is "", which names no directory`, for a
121
+ value of only space too; `probe --id ""` and `escalate --show ""`, exit 2
122
+ before, say the same line
123
+ ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
124
+ Upgrading: where a script may hand an option nothing, leave the option
125
+ out, as `${ID:+--brief "$ID"}` does in a shell. `SPEC_BRIEF` set and
126
+ empty is still as unset.
127
+ - **A memo is not written with a part that says nothing.** `escalate` with
128
+ an empty `--path`, `--reason`, `--option` or `--recommend`, and
129
+ `request_escalation` with an empty path, recommendation or option label,
130
+ recorded a request a person could not rule on: a file with no name, an
131
+ empty *Why*, `1. **** - `. Each is refused, exit 2 or a tool error that
132
+ names the argument, and nothing is recorded
133
+ ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
134
+ Upgrading: give each of them something, or leave `--option` and
135
+ `--recommend` out; an option needs a label, `"<label>: <cost>"`, and its
136
+ cost may be left out.
137
+ - **The server's tools refuse an empty `brief`, `base` or path.**
138
+ `start_round`, `check_path`, `request_escalation` and `audit_round`
139
+ answered an empty `brief` for the branch's brief and an empty `base` with
140
+ no ruling verified, and `check_path` judged an empty path as the project
141
+ itself. Each is a tool error that names the argument, `"brief" is "",
142
+ which names no brief`. Upgrading: omit an argument there is nothing to
143
+ give.
144
+ - `hook claude` refuses none of the above: whatever else is on its line is
145
+ passed over as before, since exit 2 from it holds every write of a
146
+ session. Every command line a release's `init` wrote, or its plugin
147
+ ships, is read as it was, and a test holds each
148
+ ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
149
+
150
+ ### Added
151
+
152
+ - `doctor` says when the brief a round would work under is no brief: one
153
+ spec-brief does not know or has archived, whichever of `--brief`,
154
+ `SPEC_BRIEF` or the branch names it, and an empty `--brief` or `--base`.
155
+ It printed the id and no more, `brief 999`, for a brief under which the
156
+ guard held every write. Its exit code is as it was, and `--format json`
157
+ has the reason as `briefProblem`.
158
+
159
+ ### Fixed
160
+
161
+ - A `--root` that is a file, or is not there, is refused by name, `--root
162
+ is "nowhere", which is not a directory`, where the line said the path was
163
+ outside a git work tree. Exit 2, as before.
164
+ - A `.spec-harness.json` that is a directory is said in the harness's
165
+ words, `.spec-harness.json is a directory, not a configuration file`,
166
+ where it was `is not valid JSON: EISDIR: illegal operation on a
167
+ directory, read`. Exit 2, as before.
168
+ - A memo gives an option that states no cost as the choice alone,
169
+ `2. **Refuse**`, where it ended in a dash that led nowhere.
170
+
12
171
  ## 0.11.1
13
172
 
14
173
  A probe's timeout ends the wait as well as the command, where a process out
package/README.md CHANGED
@@ -70,6 +70,13 @@ CI checks out a commit on no branch, so on a detached HEAD the branch is the one
70
70
 
71
71
  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.
72
72
 
73
+ A name that names no brief is not passed over for the next ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)):
74
+
75
+ - `--brief` given nothing, `--brief ""`, is refused, as `--brief "$ID"` is where the variable is not set: it was read as the option left out, and the branch's brief answered.
76
+ - `SPEC_BRIEF` set and empty, or to space alone, is as unset. A hook's settings and a pipeline's `env` block set it whether or not a round is under way, and the branch then names the brief, which `doctor` shows.
77
+ - 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
+ - `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
+
73
80
  ## Commands
74
81
 
75
82
  In the order a round meets them; `init` and `doctor` set a repository up and check it.
@@ -124,6 +131,8 @@ Paths are compared as the filesystem spells them, links resolved and case correc
124
131
 
125
132
  - `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.
126
133
  - `spec-harness hook git` is a pre-commit hook for any agent or none. `init --git-hook` writes it; in a hook of your own it is the line `node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git`.
134
+ - `hook claude` passes over whatever else is on its line that it does not read, as every release has, where every other command [refuses it](#an-input-that-names-nothing): its line is in settings a later release cannot rewrite, and exit 2 from it holds every write.
135
+ - 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.
127
136
 
128
137
  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)).
129
138
 
@@ -142,6 +151,10 @@ npx --no-install @descent-vtt/spec-harness rule E-012-1 --allow --note "Add rota
142
151
  git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
143
152
  ```
144
153
 
154
+ 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.
155
+
156
+ `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.
157
+
145
158
  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.
146
159
 
147
160
  - 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)).
@@ -278,14 +291,17 @@ signature: expected 401, got 200
278
291
  - A probe's commands fetch nothing through `npx` unless you say so. Given a tool's name alone where `setup` did not install the tool, `npx` would fetch the registry's package of that name and run it, unasked, since the command has no terminal to be asked on ([Names](#names)). `probe` runs a probe's commands with `npm_config_yes=false`, so npm stops there and names the package, `npx canceled due to missing packages`, and the probe is `invalid`. To have npm fetch, say `--yes` on the probe's line, or set `npm_config_yes` yourself, in lower case or upper: `probe` leaves the variable as it finds it. The variable outranks a `yes=true` in an `.npmrc`. It is npm's alone, so `pnpm dlx` fetches whatever it says, and it stops a download only: a copy an earlier fetch left in npm's cache still runs.
279
292
  - A run that proves nothing says what its command printed. Where a run failed for another reason than the probe declares, was stopped at its timeout or left no report - every run of an `invalid` probe, and any such run of a `flaky` one - `probe` shows the last 2,000 characters of its command's output, where the reason is. With the table that is on the standard error, for the first such run of each probe, under a line that names the probe and the run; with `--format json` it is `output` on each such run, and nothing is written beside the document. Colours are dropped and any other control character but the line feed and the tab is written as its escape, `\u001b`, so nothing in it reaches a terminal as a command. A `setup` that fails shows its output the same way.
280
293
  - 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)).
281
- - A command, a `setup` as well as a `run`, is waited for until its output closes, or until its timeout: `probes.timeout` seconds, or for a `run` the probe's own `timeout`. One that ends and leaves a process holding its output, as a server started in the background does, is waited for until the timeout, and is then a command that was stopped. On Windows a process the shell started with `start /b` holds that output even when its own is sent to a file.
282
- - At its timeout a command is stopped, forced, with what `probe` can reach of what it started, and no more ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)):
283
- - On Linux and macOS that is every process still in the command's process group: all the command started, except what moved to a session or a group of its own, as a daemon does, and a process Node starts `detached`.
284
- - On Windows it is the command's shell and the processes under it, each found through a parent that still runs. A process whose parent has ended is not found, and once the shell itself has ended, as it has when its line only started something with `start /b`, nothing the command left is stopped.
285
- - What is out of reach runs on until it ends or you end it.
286
- - The timeout ends the wait as well. Three seconds after it has stopped what it can, the run is answered whether or not something still holds the command's output: it is a run stopped at its timeout, with what was printed until then, and its evidence ends `, and something it started was left running, holding its output`. A `setup` answered so says the same in the error that stops the probe. `probe` reads no more of that output, so a process that writes to it afterwards has the write fail, as it would once `probe` had ended, and a later run starts while that process still runs.
287
- - Interrupted - by Ctrl+C, SIGTERM or SIGHUP, or by Ctrl+Break on Windows - `probe` stops the commands still running, with the same reach and no more, waits up to three seconds for their output, and removes the worktree before it exits.
288
- - Neither a timeout nor an interrupt stops what an earlier command left running once that command had ended and its output had closed: `probe` holds nothing of it by then.
294
+ - A command, a `setup` as well as a `run`, is its shell, as a step is in CI: it is answered when its shell ends, by the shell's exit code and what was printed until then. A `run` is red, green or neither by those, and a `setup` has passed when the code is 0. What the command started and did not wait for is no part of the answer, so a command's work must be done when its shell ends: have it wait for what it starts.
295
+ - Where something the command started still holds its output once the shell has ended, as a server left in the background does, `probe` waits three seconds for that output, then reads no more of it and goes on; GitHub's runner does the same with a step, after five. The run's evidence then ends `, and something it started was left running, holding its output`. A `setup` that passed says so in a line on the standard error, `spec-harness: the probe setup "<line>" passed at base, and something it started was left running, holding its output`, and one that failed says it in the error that stops the probe. A process that writes to that output afterwards has the write fail, as it would once `probe` had ended. Send a process's output to a file and nothing is held, nothing waited for and nothing said; on Windows a process the shell started with `start /b` holds the command's output even so.
296
+ - What a command left running runs on beside the commands after it, as what a step left does in CI, and is stopped when `probe` is done at that commit, before the worktree is removed, as far as `probe` can still reach it ([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)):
297
+ - On Linux and macOS that is every process still in the command's process group, forced: what the shell put in the background with `&`, whether or not it holds the command's output. Not what moved to a session or a group of its own, as a daemon does, `setsid`, and a process Node starts `detached`.
298
+ - On Windows it is nothing: once a command's shell has ended, nothing the command left can be found again.
299
+ - What is out of reach runs on until it ends or you end it: through the runs after it, and through the head's when it was left at the base.
300
+ - A command whose shell still runs at its timeout - `probes.timeout` seconds, or for a `run` the probe's own `timeout` - is stopped, forced, with what `probe` can reach of what it started, and no more:
301
+ - On Linux and macOS that is every process still in the command's process group: all the command started, except what moved to a session or a group of its own.
302
+ - On Windows it is the command's shell and the processes under it, each found through a parent that still runs. A process whose parent has ended is not found.
303
+ - The timeout ends the wait as well. Three seconds after it has stopped what it can, the run is answered whether or not something still holds the command's output: it is a run stopped at its timeout, with what was printed until then, and its evidence says the same of what was left running. A `setup` answered so says it in the error that stops the probe.
304
+ - Interrupted - by Ctrl+C, SIGTERM or SIGHUP, or by Ctrl+Break on Windows - `probe` stops the commands still running, with the reach a timeout has, waits up to three seconds for their output, stops what earlier commands left, with the reach the end of a commit has, and removes the worktree before it exits.
289
305
  - A worktree's directory that cannot be deleted - on Windows, one that something a command started is still running in - costs the verdict nothing: git forgets the worktree, `probe` prints and exits as it would have, and it names the directory it left on the standard error as it exits.
290
306
 
291
307
  ### `premises`
@@ -295,13 +311,17 @@ Is every live brief still about something true? It runs spec-guard over the live
295
311
  - Goals are left to `audit`, since a goal fails until its round is done.
296
312
  - 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.
297
313
  - 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.
314
+ - A brief `--brief` or `SPEC_BRIEF` names that spec-brief does not know is refused, exit 2, as every command that acts on a brief refuses it: read as no round, it held the round's own premise against it.
315
+ - A branch whose name reads as a brief's and names none is read as no round, as before, and so is a brief spec-brief has archived. CI runs `premises` on every branch, and `fix/brief-list-order`, which names `list` under the default `branches`, is no round's; an archived brief is a closed round, which retires no premise, and is what the branch of a round names once the round has archived it.
298
316
 
299
- 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.
317
+ Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not there to ask, or `--brief` or `SPEC_BRIEF` names a brief that is not one. `--format gitlab`, `sarif` or `github` prints its findings for a forge, as `audit` does.
300
318
 
301
319
  ### `mcp`
302
320
 
303
321
  `start_round`, `check_path`, `request_escalation`, `audit_round` and `list_rounds` over MCP on stdio, with the four workflow prompts. Both protocol eras are served. The architecture rules themselves stay with spec-guard's server.
304
322
 
323
+ A tool answers an argument it cannot use with a tool error that names the argument, and writes nothing: one it does not take, one of the wrong type, and one that is given and [names nothing](#an-input-that-names-nothing), such as an empty `brief`.
324
+
305
325
  ### `init`
306
326
 
307
327
  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.
@@ -340,6 +360,8 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
340
360
  - A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1.
341
361
  - A command named under `tools` is run as named, and its version is not checked.
342
362
  - **The repository**: its root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names.
363
+ - 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.
364
+ - 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.
343
365
  - **What a signed ruling needs to count**:
344
366
  - the base, and whether `--base`, `.spec-harness.json` or the remote named it;
345
367
  - whether the allowed-signers file is on that base;
@@ -357,6 +379,42 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
357
379
  - **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.
358
380
  - 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`.
359
381
 
382
+ ### An input that names nothing
383
+
384
+ An option, an argument or a value that is given and names nothing is refused: exit 2, one line on stderr that names it and says what to do, nothing read or written for it. None is read as if it had not been given. `--root "$DIR"` where the variable is not set would otherwise run in the current directory, and the answer would look like the one that was asked for ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
385
+
386
+ - **An option a command does not read.** A command takes the options its answer depends on, and refuses any other by name, with the ones it takes: `doctor does not take --note; its options are --brief, --base, --root, --format and --strict`.
387
+
388
+ | Command | Reads |
389
+ | --- | --- |
390
+ | `context`, `rulings` | `--brief`, `--base`, `--root`, `--format` |
391
+ | `guard`, `audit`, `doctor` | `--brief`, `--base`, `--root`, `--format`, `--strict` |
392
+ | `probe` | `--brief`, `--base`, `--root`, `--format`, `--id`, `--at` |
393
+ | `premises` | `--brief`, `--root`, `--format`, `--strict` |
394
+ | `escalate` | `--brief`, `--root`, `--format`, `--path`, `--reason`, `--option`, `--recommend`, `--list`, `--show` |
395
+ | `rule` | `--root`, `--format`, `--allow`, `--deny`, `--note` |
396
+ | `init` | `--root`, `--format`, `--write`, `--git-hook` |
397
+ | `hook` | `--brief`, `--base`, `--root` |
398
+ | `mcp` | `--root` |
399
+
400
+ `init` chooses the base itself, and the server is told the brief and the base by each call, so neither takes `--base` or `--brief`: taking one would say the base or the brief had been set.
401
+ - **An option beside one that leaves it unread.** `escalate --list` and `escalate --show <id>` print what waits and raise nothing, and beside either only `--root` and `--format` are read: `escalate --list does not take --path; beside --list its options are --root and --format`. A request spelled out beside `--list` printed the list, exit 0, and was never raised.
402
+ - **A brief named twice.** `context`, `rulings`, `audit` and `probe` take the brief as their argument or as `--brief`, and refuse the two together, whatever they hold: `audit was given the brief twice, as its argument "012" and as --brief "013"; give one of them`. The argument won with no word. `SPEC_BRIEF` and the branch beside either are the [order](#which-brief) as it stands.
403
+ - **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.
404
+ - **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.
405
+ - **A `--root` that is no directory**, a file or a path that is not there: `--root is "nowhere", which is not a directory`.
406
+ - **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.
407
+
408
+ `--help` and `--version` answer whatever else is on the line. Three things are read another way, each for its reason:
409
+
410
+ - **`hook claude` reads its line as every release has.** The line is in `.claude/settings.json` and in the Claude Code plugin of every release since 0.1.0, which a later release cannot rewrite, and exit 2 from it holds every write of a session, the one that would mend the line among them. So what it does not read is passed over as before, an empty `--brief` with the rest; an option no command has is exit 2 there, as it always was. The releases have handed the harness five command lines, `hook claude`, `hook git`, `mcp`, and `mcp --root` with the project or with `.`, and each is read as the release that wrote it read it ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
411
+
412
+ One thing on its line is said, and still refuses nothing: a `--root` that is a file, or is not there. It read as a session outside a git work tree, exit 0 and no word, and the guard was off. It is now one line on stderr and exit 1, `--root is "nowhere", which is not a directory, so the guard is off and this write is not checked`, with what to do. Claude Code does not block a tool call on exit 1: it goes on with the write and shows the line as a hook error in the transcript. No release wrote a `--root` on the hook's line, so this is a line a person changed. An empty `--root` is the directory the hook was started in, and guards there, as before.
413
+ - **`doctor` says an empty `--brief` or `--base` in its report**, as it says a brief spec-brief does not know: a diagnosis is what a person runs when something is wrong, and it reports what is wrong with what it was given.
414
+ - **`SPEC_BRIEF` set and empty is as unset**, since what sets it sets it whether or not there is a round ([Which brief](#which-brief)).
415
+
416
+ **In a tool call.** The server's tools refuse the same in their arguments, with a tool error that names the argument, as in `"paths[1]" is "", which names no file`: an empty `brief` or `base`, an empty path, an empty `recommendation`, an option whose `label` is empty. An argument a tool does not take was refused already. A `brief` spec-brief does not know, or has archived, is said of the argument too, `"brief" names brief 999, and spec-brief knows no such brief`, where the line called it the flag.
417
+
360
418
  ## As a Claude Code plugin
361
419
 
362
420
  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)).
@@ -432,7 +490,7 @@ A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-har
432
490
  | --- | --- |
433
491
  | `0` | Clean. |
434
492
  | `1` | Refused, found something, or waiting on a person. |
435
- | `2` | The answer cannot be trusted, or did not arrive. A git command that failed is one, and so is a stdout its reader closed before all of the output was written, as a pipeline into `head` does: each is one line on stderr. An error the harness did not expect is one too: `spec-harness: unexpected error:` and its stack, on stderr. |
493
+ | `2` | The answer cannot be trusted, or did not arrive. An option, an argument or a value that [names nothing](#an-input-that-names-nothing) is one. A git command that failed is one, and so is a stdout its reader closed before all of the output was written, as a pipeline into `head` does: each is one line on stderr. An error the harness did not expect is one too: `spec-harness: unexpected error:` and its stack, on stderr. |
436
494
  | `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
437
495
  | `143` | A `probe` interrupted by SIGTERM. |
438
496
  | `129` | A `probe` interrupted by SIGHUP. |
package/dist/briefs.d.ts CHANGED
@@ -19,8 +19,12 @@ export declare class SiblingOutputError extends Error {
19
19
  }
20
20
  /** Reads the document `spec-brief list --format json` prints. */
21
21
  export declare function parseBriefList(document: unknown): BriefRow[];
22
- /** How the active brief was named, for messages. */
23
- export type ActiveSource = 'flag' | 'environment' | 'branch';
22
+ /**
23
+ * How the active brief was named, for messages. An argument is the id a
24
+ * command takes after its name, or a tool call's `brief`: said to be the
25
+ * flag's, it sent a person looking for a `--brief` nobody had typed.
26
+ */
27
+ export type ActiveSource = 'argument' | 'flag' | 'environment' | 'branch';
24
28
  export type ActiveBrief = {
25
29
  readonly kind: 'found';
26
30
  readonly brief: BriefRow;
@@ -38,11 +42,12 @@ export type ActiveBrief = {
38
42
  readonly source: ActiveSource;
39
43
  };
40
44
  /**
41
- * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by
42
- * the branch - never guessed from which brief happens to be the only active
43
- * one (ADR-0004).
45
+ * The brief a round is working on: named by an argument, by a flag, by
46
+ * `SPEC_BRIEF`, or by the branch - never guessed from which brief happens to
47
+ * be the only active one (ADR-0004).
44
48
  */
45
49
  export declare function findActive(briefs: readonly BriefRow[], named: {
50
+ readonly argument?: string | undefined;
46
51
  readonly flag?: string | undefined;
47
52
  readonly environment?: string | undefined;
48
53
  readonly branch?: string | null | undefined;
package/dist/briefs.js CHANGED
@@ -84,9 +84,9 @@ export function parseBriefList(document) {
84
84
  });
85
85
  }
86
86
  /**
87
- * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by
88
- * the branch - never guessed from which brief happens to be the only active
89
- * one (ADR-0004).
87
+ * The brief a round is working on: named by an argument, by a flag, by
88
+ * `SPEC_BRIEF`, or by the branch - never guessed from which brief happens to
89
+ * be the only active one (ADR-0004).
90
90
  */
91
91
  export function findActive(briefs, named) {
92
92
  const pick = (id, source) => {
@@ -99,6 +99,8 @@ export function findActive(briefs, named) {
99
99
  return { kind: 'archived', brief: archived, source };
100
100
  return { kind: 'unknown', id, source };
101
101
  };
102
+ if (named.argument !== undefined && named.argument.trim() !== '')
103
+ return pick(named.argument.trim(), 'argument');
102
104
  if (named.flag !== undefined && named.flag.trim() !== '')
103
105
  return pick(named.flag.trim(), 'flag');
104
106
  if (named.environment !== undefined && named.environment.trim() !== '')
@@ -1 +1 @@
1
- {"version":3,"file":"briefs.js","sourceRoot":"","sources":["../src/briefs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The briefs, as spec-brief reports them, and which one a round is working on.\n *\n * spec-brief owns the brief format and its configuration (its ADR-0003), so\n * the harness reads briefs the way spec-brief does by asking it:\n * `spec-brief list --archived --format json`. This module reads that\n * document and nothing else; a shape it does not recognise is an error, never\n * a best-effort reading of half the fields (ADR-0002).\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\n/**\n * The spec-brief JSON document versions this harness reads. Version 1 is the\n * one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the\n * harness never reads, so the `list` and `archive` documents are the same.\n */\nexport const SUPPORTED_SCHEMA_VERSIONS: readonly number[] = [1, 2, 3];\n\nexport class SiblingOutputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingOutputError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction strings(value: unknown, where: string): string[] {\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {\n throw new SiblingOutputError(`${where} is not a list of strings`);\n }\n return value;\n}\n\nfunction nullableString(value: unknown, where: string): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') throw new SiblingOutputError(`${where} is not a string`);\n return value;\n}\n\n/** Reads the document `spec-brief list --format json` prints. */\nexport function parseBriefList(document: unknown): BriefRow[] {\n if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {\n throw new SiblingOutputError('spec-brief did not print a list document');\n }\n const version = document['schemaVersion'];\n // The list holds numbers alone, so `includes` refuses whatever is not one:\n // the `typeof` is the compiler's, and the mutant without it is equivalent.\n if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {\n throw new SiblingOutputError(\n `spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`,\n );\n }\n const briefs = document['briefs'];\n if (!Array.isArray(briefs)) throw new SiblingOutputError('spec-brief list has no \"briefs\"');\n return briefs.map((row, index) => {\n const where = `briefs[${index}]`;\n if (!isObject(row)) throw new SiblingOutputError(`${where} is not an object`);\n const id = row['id'];\n const file = row['file'];\n if (typeof id !== 'string' || typeof file !== 'string') throw new SiblingOutputError(`${where} has no id or file`);\n const phase = row['phase'];\n if (phase !== 'live' && phase !== 'archived') throw new SiblingOutputError(`${where}.phase is \"${String(phase)}\"`);\n const tasks = row['tasks'];\n const wave = row['wave'];\n return {\n id,\n file,\n title: nullableString(row['title'], `${where}.title`),\n phase,\n status: nullableString(row['status'], `${where}.status`),\n type: nullableString(row['type'], `${where}.type`),\n wave: typeof wave === 'number' ? wave : null,\n dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),\n affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),\n protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),\n tasks: {\n total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,\n checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,\n },\n ready: row['ready'] === true,\n waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),\n };\n });\n}\n\n/** How the active brief was named, for messages. */\nexport type ActiveSource = 'flag' | 'environment' | 'branch';\n\nexport type ActiveBrief =\n | { readonly kind: 'found'; readonly brief: BriefRow; readonly source: ActiveSource }\n | { readonly kind: 'none'; readonly reason: string }\n | { readonly kind: 'unknown'; readonly id: string; readonly source: ActiveSource }\n | { readonly kind: 'archived'; readonly brief: BriefRow; readonly source: ActiveSource };\n\n/**\n * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by\n * the branch - never guessed from which brief happens to be the only active\n * one (ADR-0004).\n */\nexport function findActive(\n briefs: readonly BriefRow[],\n named: { readonly flag?: string | undefined; readonly environment?: string | undefined; readonly branch?: string | null | undefined },\n): ActiveBrief {\n const pick = (id: string, source: ActiveSource): ActiveBrief => {\n const matches = briefs.filter((brief) => sameId(brief.id, id));\n const live = matches.find((brief) => brief.phase === 'live');\n if (live !== undefined) return { kind: 'found', brief: live, source };\n const archived = matches[0];\n if (archived !== undefined) return { kind: 'archived', brief: archived, source };\n return { kind: 'unknown', id, source };\n };\n if (named.flag !== undefined && named.flag.trim() !== '') return pick(named.flag.trim(), 'flag');\n if (named.environment !== undefined && named.environment.trim() !== '') return pick(named.environment.trim(), 'environment');\n if (named.branch !== undefined && named.branch !== null) return pick(named.branch, 'branch');\n return {\n kind: 'none',\n reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',\n };\n}\n"]}
1
+ {"version":3,"file":"briefs.js","sourceRoot":"","sources":["../src/briefs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The briefs, as spec-brief reports them, and which one a round is working on.\n *\n * spec-brief owns the brief format and its configuration (its ADR-0003), so\n * the harness reads briefs the way spec-brief does by asking it:\n * `spec-brief list --archived --format json`. This module reads that\n * document and nothing else; a shape it does not recognise is an error, never\n * a best-effort reading of half the fields (ADR-0002).\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\n/**\n * The spec-brief JSON document versions this harness reads. Version 1 is the\n * one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the\n * harness never reads, so the `list` and `archive` documents are the same.\n */\nexport const SUPPORTED_SCHEMA_VERSIONS: readonly number[] = [1, 2, 3];\n\nexport class SiblingOutputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingOutputError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction strings(value: unknown, where: string): string[] {\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {\n throw new SiblingOutputError(`${where} is not a list of strings`);\n }\n return value;\n}\n\nfunction nullableString(value: unknown, where: string): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') throw new SiblingOutputError(`${where} is not a string`);\n return value;\n}\n\n/** Reads the document `spec-brief list --format json` prints. */\nexport function parseBriefList(document: unknown): BriefRow[] {\n if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {\n throw new SiblingOutputError('spec-brief did not print a list document');\n }\n const version = document['schemaVersion'];\n // The list holds numbers alone, so `includes` refuses whatever is not one:\n // the `typeof` is the compiler's, and the mutant without it is equivalent.\n if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {\n throw new SiblingOutputError(\n `spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`,\n );\n }\n const briefs = document['briefs'];\n if (!Array.isArray(briefs)) throw new SiblingOutputError('spec-brief list has no \"briefs\"');\n return briefs.map((row, index) => {\n const where = `briefs[${index}]`;\n if (!isObject(row)) throw new SiblingOutputError(`${where} is not an object`);\n const id = row['id'];\n const file = row['file'];\n if (typeof id !== 'string' || typeof file !== 'string') throw new SiblingOutputError(`${where} has no id or file`);\n const phase = row['phase'];\n if (phase !== 'live' && phase !== 'archived') throw new SiblingOutputError(`${where}.phase is \"${String(phase)}\"`);\n const tasks = row['tasks'];\n const wave = row['wave'];\n return {\n id,\n file,\n title: nullableString(row['title'], `${where}.title`),\n phase,\n status: nullableString(row['status'], `${where}.status`),\n type: nullableString(row['type'], `${where}.type`),\n wave: typeof wave === 'number' ? wave : null,\n dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),\n affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),\n protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),\n tasks: {\n total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,\n checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,\n },\n ready: row['ready'] === true,\n waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),\n };\n });\n}\n\n/**\n * How the active brief was named, for messages. An argument is the id a\n * command takes after its name, or a tool call's `brief`: said to be the\n * flag's, it sent a person looking for a `--brief` nobody had typed.\n */\nexport type ActiveSource = 'argument' | 'flag' | 'environment' | 'branch';\n\nexport type ActiveBrief =\n | { readonly kind: 'found'; readonly brief: BriefRow; readonly source: ActiveSource }\n | { readonly kind: 'none'; readonly reason: string }\n | { readonly kind: 'unknown'; readonly id: string; readonly source: ActiveSource }\n | { readonly kind: 'archived'; readonly brief: BriefRow; readonly source: ActiveSource };\n\n/**\n * The brief a round is working on: named by an argument, by a flag, by\n * `SPEC_BRIEF`, or by the branch - never guessed from which brief happens to\n * be the only active one (ADR-0004).\n */\nexport function findActive(\n briefs: readonly BriefRow[],\n named: {\n readonly argument?: string | undefined;\n readonly flag?: string | undefined;\n readonly environment?: string | undefined;\n readonly branch?: string | null | undefined;\n },\n): ActiveBrief {\n const pick = (id: string, source: ActiveSource): ActiveBrief => {\n const matches = briefs.filter((brief) => sameId(brief.id, id));\n const live = matches.find((brief) => brief.phase === 'live');\n if (live !== undefined) return { kind: 'found', brief: live, source };\n const archived = matches[0];\n if (archived !== undefined) return { kind: 'archived', brief: archived, source };\n return { kind: 'unknown', id, source };\n };\n if (named.argument !== undefined && named.argument.trim() !== '') return pick(named.argument.trim(), 'argument');\n if (named.flag !== undefined && named.flag.trim() !== '') return pick(named.flag.trim(), 'flag');\n if (named.environment !== undefined && named.environment.trim() !== '') return pick(named.environment.trim(), 'environment');\n if (named.branch !== undefined && named.branch !== null) return pick(named.branch, 'branch');\n return {\n kind: 'none',\n reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',\n };\n}\n"]}
package/dist/cli.d.ts CHANGED
@@ -2,15 +2,16 @@
2
2
  * The command line: arguments, dispatch, output and exit codes.
3
3
  *
4
4
  * Exit 0: done, nothing refused. Exit 1: something refused or found. Exit 2:
5
- * the answer cannot be trusted - a bad flag, a configuration that does not
6
- * load, a sibling that is missing or printed something unreadable, a git
7
- * command that failed, an output its reader closed, an error nothing here
8
- * expected. A run that could not look must never exit as though it looked and
9
- * found nothing (spec-core ADR-0005).
5
+ * the answer cannot be trusted - a bad flag, an option, an argument or a
6
+ * value that names nothing, a configuration that does not load, a sibling
7
+ * that is missing or printed something unreadable, a git command that
8
+ * failed, an output its reader closed, an error nothing here expected. A run
9
+ * that could not look must never exit as though it looked and found nothing
10
+ * (spec-core ADR-0005).
10
11
  */
11
12
  import { type CliIO } from './workspace.js';
12
13
  export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
13
- export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies\n and the install scripts it allowed.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at base|head|both, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice, or Claude Code is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch.\n --root <dir> Run from another directory.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n --strict Warnings fail the run.\n --help, --version\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
14
+ export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo. Neither\n raises a request: beside one, only --root and --format are read.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note <text>. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies\n and the install scripts it allowed.\n probe [brief] Run the brief's probes: red at the base, green at the head.\n --at <base|head|both>, --id <probe>.\n premises Do the live briefs' premises still hold? For CI: exit 1 when one\n is stale, or, with --strict, when spec-guard cannot read one.\n init Configure the spec-* tools to agree. Prints the plan; --write\n applies it; --git-hook adds a pre-commit hook.\n mcp Serve start_round, check_path, request_escalation, audit_round,\n list_rounds and the workflow prompts over MCP on stdio.\n doctor Which siblings are installed, at which versions, which brief is\n named, how Claude Code runs the guard, and git's hook. Exit 1\n when a sibling is older than this release needs, the guard is\n installed twice, or Claude Code is too old to run its hooks.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n For context, guard, hook, escalate, rulings, audit, probe, premises\n and doctor.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch. For context,\n guard, hook, rulings, audit, probe and doctor.\n --root <dir> Run from another directory, which must exist. For every command.\n --format <fmt> pretty or json; audit and premises also gitlab, sarif or github.\n For every command but hook and mcp.\n --strict Warnings fail the run. For guard, audit, premises and doctor.\n --help, --version\n\nAn option a command does not read, an argument it does not take, an option or an\nargument given an empty value, and a brief given both as the argument and as --brief\nare refused, exit 2, not read as if they were not there. doctor says an empty --brief\nor --base in its report. hook claude reads its line as every release has, and says a\n--root that is not a directory with exit 1, refusing nothing.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
14
15
  /**
15
16
  * Runs the command line and resolves to the exit code, for an error nothing
16
17
  * below expected too.