@descent-vtt/spec-harness 0.11.0 → 0.12.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 +143 -0
- package/README.md +57 -2
- package/bin/spec-harness.js +8 -1
- package/dist/cli.d.ts +15 -5
- package/dist/cli.js +102 -31
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +37 -5
- package/dist/commands.js.map +1 -1
- package/dist/fs.d.ts +2 -0
- package/dist/fs.js +16 -1
- package/dist/fs.js.map +1 -1
- package/dist/rulings.js +3 -1
- package/dist/rulings.js.map +1 -1
- package/dist/sandbox.d.ts +31 -3
- package/dist/sandbox.js +108 -9
- package/dist/sandbox.js.map +1 -1
- package/dist/server.js +81 -22
- package/dist/server.js.map +1 -1
- package/dist/usage.d.ts +210 -0
- package/dist/usage.js +148 -0
- package/dist/usage.js.map +1 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +1 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,149 @@ 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.12.0
|
|
13
|
+
|
|
14
|
+
A probe's command is answered when its shell ends, by the shell's exit code,
|
|
15
|
+
where one that left something holding its output was waited for until its
|
|
16
|
+
timeout and then called stopped. And an input that is set and names nothing
|
|
17
|
+
is refused by a line that names it, where it was read as if it had not been
|
|
18
|
+
given: an option another command reads, an argument a command does not
|
|
19
|
+
take, an empty `--root`, `--brief` or `--base`, an escalation with an empty
|
|
20
|
+
path or reason, from the command line or through the server. Every command
|
|
21
|
+
line a release's `init` wrote is read as it was.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **A probe's command is answered when its shell ends, by the shell's exit
|
|
26
|
+
code.** A command that ended and left something holding its output, as a
|
|
27
|
+
server started in the background does, was waited for until its timeout,
|
|
28
|
+
ten minutes by default, and then counted as stopped: its `run` was a
|
|
29
|
+
`timeout` and the probe `invalid`, and its `setup` stopped the probe.
|
|
30
|
+
`probe` now waits three seconds for that output, as GitHub's runner waits
|
|
31
|
+
five for a step's, judges the command by its exit code and what it had
|
|
32
|
+
printed, and says that something it started was left running
|
|
33
|
+
([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
|
|
34
|
+
Upgrading: a command's work must be done when its shell ends, so have it
|
|
35
|
+
wait for what it starts. A probe that was `invalid` at its timeout may now
|
|
36
|
+
be `measured`, `vacuous`, `fixed` or `still-failing`, and a `setup` that
|
|
37
|
+
stopped the probe may pass.
|
|
38
|
+
- On Linux and macOS, what a probe's command left running in its process
|
|
39
|
+
group is stopped when `probe` is done at that commit, before the worktree
|
|
40
|
+
is removed. One that held the command's output was stopped at the
|
|
41
|
+
command's timeout, and one that held nothing ran on after `probe` had
|
|
42
|
+
ended. Upgrading: a process a `setup` starts in the background serves the
|
|
43
|
+
runs at that commit and does not outlive them. On Windows it still runs
|
|
44
|
+
on, since nothing a command left can be reached there.
|
|
45
|
+
- **An option a command does not read is refused by that command.** Every
|
|
46
|
+
command took every option, so `doctor --note x`, `init --base main`,
|
|
47
|
+
`mcp --brief 012` and `hook git --strict` ran as if the option were not
|
|
48
|
+
there. Each is now exit 2 and one line that names it, `doctor does not
|
|
49
|
+
take --note; its options are --brief, --base, --root, --format and
|
|
50
|
+
--strict`
|
|
51
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md),
|
|
52
|
+
[An input that names nothing](README.md#an-input-that-names-nothing)).
|
|
53
|
+
Upgrading: drop the option. The README's table has what each command
|
|
54
|
+
reads; `--strict` is for `guard`, `audit`, `premises` and `doctor`, and
|
|
55
|
+
`init` and `mcp` take neither `--brief` nor `--base`.
|
|
56
|
+
- **An argument a command does not take is refused.** `doctor extra`,
|
|
57
|
+
`premises extra` and `context 012 extra` ran as if it were not there, and
|
|
58
|
+
`init mydir` set up the directory it stood in; each is exit 2 with the
|
|
59
|
+
argument named. Upgrading: drop the argument; `init --root mydir` sets up
|
|
60
|
+
another directory.
|
|
61
|
+
- **An option or an argument given an empty value is refused by name, not
|
|
62
|
+
read as left out.** `--root ""` ran in the current directory, `--brief ""`
|
|
63
|
+
was answered for the branch's brief, `audit --base ""` measured no change
|
|
64
|
+
and warned, and `guard src/a.ts ""` judged the repository itself. Each is
|
|
65
|
+
exit 2 and a line such as `--root is "", which names no directory`, for a
|
|
66
|
+
value of only space too; `probe --id ""` and `escalate --show ""`, exit 2
|
|
67
|
+
before, say the same line
|
|
68
|
+
([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
|
|
69
|
+
Upgrading: where a script may hand an option nothing, leave the option
|
|
70
|
+
out, as `${ID:+--brief "$ID"}` does in a shell. `SPEC_BRIEF` set and
|
|
71
|
+
empty is still as unset.
|
|
72
|
+
- **A memo is not written with a part that says nothing.** `escalate` with
|
|
73
|
+
an empty `--path`, `--reason`, `--option` or `--recommend`, and
|
|
74
|
+
`request_escalation` with an empty path, recommendation or option label,
|
|
75
|
+
recorded a request a person could not rule on: a file with no name, an
|
|
76
|
+
empty *Why*, `1. **** - `. Each is refused, exit 2 or a tool error that
|
|
77
|
+
names the argument, and nothing is recorded
|
|
78
|
+
([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
|
|
79
|
+
Upgrading: give each of them something, or leave `--option` and
|
|
80
|
+
`--recommend` out; an option needs a label, `"<label>: <cost>"`, and its
|
|
81
|
+
cost may be left out.
|
|
82
|
+
- **The server's tools refuse an empty `brief`, `base` or path.**
|
|
83
|
+
`start_round`, `check_path`, `request_escalation` and `audit_round`
|
|
84
|
+
answered an empty `brief` for the branch's brief and an empty `base` with
|
|
85
|
+
no ruling verified, and `check_path` judged an empty path as the project
|
|
86
|
+
itself. Each is a tool error that names the argument, `"brief" is "",
|
|
87
|
+
which names no brief`. Upgrading: omit an argument there is nothing to
|
|
88
|
+
give.
|
|
89
|
+
- `hook claude` refuses none of the above: whatever else is on its line is
|
|
90
|
+
passed over as before, since exit 2 from it holds every write of a
|
|
91
|
+
session. Every command line a release's `init` wrote, or its plugin
|
|
92
|
+
ships, is read as it was, and a test holds each
|
|
93
|
+
([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- `doctor` says when the brief a round would work under is no brief: one
|
|
98
|
+
spec-brief does not know or has archived, whichever of `--brief`,
|
|
99
|
+
`SPEC_BRIEF` or the branch names it, and an empty `--brief` or `--base`.
|
|
100
|
+
It printed the id and no more, `brief 999`, for a brief under which the
|
|
101
|
+
guard held every write. Its exit code is as it was, and `--format json`
|
|
102
|
+
has the reason as `briefProblem`.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- A `--root` that is a file, or is not there, is refused by name, `--root
|
|
107
|
+
is "nowhere", which is not a directory`, where the line said the path was
|
|
108
|
+
outside a git work tree. Exit 2, as before.
|
|
109
|
+
- A `.spec-harness.json` that is a directory is said in the harness's
|
|
110
|
+
words, `.spec-harness.json is a directory, not a configuration file`,
|
|
111
|
+
where it was `is not valid JSON: EISDIR: illegal operation on a
|
|
112
|
+
directory, read`. Exit 2, as before.
|
|
113
|
+
- A memo gives an option that states no cost as the choice alone,
|
|
114
|
+
`2. **Refuse**`, where it ended in a dash that led nowhere.
|
|
115
|
+
|
|
116
|
+
## 0.11.1
|
|
117
|
+
|
|
118
|
+
A probe's timeout ends the wait as well as the command, where a process out
|
|
119
|
+
of reach that held the command's output kept the probe waiting without
|
|
120
|
+
limit; and an exit 2 the harness could foresee says so in a line.
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
|
|
124
|
+
- **A reader that closes the output is answered in one line, not a stack.**
|
|
125
|
+
`spec-harness context | head` could end with `spec-harness: unexpected
|
|
126
|
+
error: Error: EPIPE: broken pipe, write` and a stack once `head` had left;
|
|
127
|
+
it now prints `spec-harness: stdout was closed before all of the output
|
|
128
|
+
was written` on stderr, from every command and the server. The exit code
|
|
129
|
+
is 2, as it was
|
|
130
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
131
|
+
- A git command that failed - the diff `audit` reads, the staged files
|
|
132
|
+
`hook git` reads, the worktree `probe` adds - is reported as what git
|
|
133
|
+
said, on one line: `spec-harness: git diff --cached failed: fatal: ...`.
|
|
134
|
+
It was `unexpected error: GitError:` and a stack. Exit 2, as before.
|
|
135
|
+
- A probe's timeout ends the wait for its command. Where a process the
|
|
136
|
+
command started still held its output and was out of the timeout's reach -
|
|
137
|
+
one in a session of its own, or on Windows anything left by a command whose
|
|
138
|
+
shell had ended - `probe` waited for that process to end, and never ended
|
|
139
|
+
while it ran. The command is now answered three seconds after its timeout,
|
|
140
|
+
as stopped at it, with what it had printed, and the run's evidence says
|
|
141
|
+
that something it started was left running
|
|
142
|
+
([ADR-0003](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0003-state-outside-the-work-tree.md)).
|
|
143
|
+
- A probe's `setup` that was stopped at its timeout says so, and after how
|
|
144
|
+
many seconds. It said only that the setup failed, with whatever the setup
|
|
145
|
+
had printed.
|
|
146
|
+
|
|
147
|
+
### Documentation
|
|
148
|
+
|
|
149
|
+
- The README said an interrupted `probe` stops every command "with
|
|
150
|
+
everything those started". It and ADR-0003 now say what a timeout and an
|
|
151
|
+
interrupt reach - the command's process group on Linux and macOS, the
|
|
152
|
+
processes under the command's shell on Windows while that shell runs - and
|
|
153
|
+
what they leave running.
|
|
154
|
+
|
|
12
155
|
## 0.11.0
|
|
13
156
|
|
|
14
157
|
An error the harness did not expect is exit 2, where it was Node's exit 1; a
|
package/README.md
CHANGED
|
@@ -70,6 +70,12 @@ 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.
|
|
78
|
+
|
|
73
79
|
## Commands
|
|
74
80
|
|
|
75
81
|
In the order a round meets them; `init` and `doctor` set a repository up and check it.
|
|
@@ -124,6 +130,7 @@ Paths are compared as the filesystem spells them, links resolved and case correc
|
|
|
124
130
|
|
|
125
131
|
- `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
132
|
- `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`.
|
|
133
|
+
- `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.
|
|
127
134
|
|
|
128
135
|
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
136
|
|
|
@@ -142,6 +149,8 @@ npx --no-install @descent-vtt/spec-harness rule E-012-1 --allow --note "Add rota
|
|
|
142
149
|
git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
|
|
143
150
|
```
|
|
144
151
|
|
|
152
|
+
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.
|
|
153
|
+
|
|
145
154
|
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
155
|
|
|
147
156
|
- 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,7 +287,17 @@ signature: expected 401, got 200
|
|
|
278
287
|
- 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
288
|
- 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
289
|
- 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
|
-
-
|
|
290
|
+
- 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.
|
|
291
|
+
- 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.
|
|
292
|
+
- 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)):
|
|
293
|
+
- 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`.
|
|
294
|
+
- On Windows it is nothing: once a command's shell has ended, nothing the command left can be found again.
|
|
295
|
+
- 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.
|
|
296
|
+
- 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:
|
|
297
|
+
- 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.
|
|
298
|
+
- 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.
|
|
299
|
+
- 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.
|
|
300
|
+
- 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.
|
|
282
301
|
- 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.
|
|
283
302
|
|
|
284
303
|
### `premises`
|
|
@@ -295,6 +314,8 @@ Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not ther
|
|
|
295
314
|
|
|
296
315
|
`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.
|
|
297
316
|
|
|
317
|
+
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`.
|
|
318
|
+
|
|
298
319
|
### `init`
|
|
299
320
|
|
|
300
321
|
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.
|
|
@@ -333,6 +354,8 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
333
354
|
- A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1.
|
|
334
355
|
- A command named under `tools` is run as named, and its version is not checked.
|
|
335
356
|
- **The repository**: its root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names.
|
|
357
|
+
- 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.
|
|
358
|
+
- 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.
|
|
336
359
|
- **What a signed ruling needs to count**:
|
|
337
360
|
- the base, and whether `--base`, `.spec-harness.json` or the remote named it;
|
|
338
361
|
- whether the allowed-signers file is on that base;
|
|
@@ -350,6 +373,38 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
350
373
|
- **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.
|
|
351
374
|
- 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`.
|
|
352
375
|
|
|
376
|
+
### An input that names nothing
|
|
377
|
+
|
|
378
|
+
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)).
|
|
379
|
+
|
|
380
|
+
- **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`.
|
|
381
|
+
|
|
382
|
+
| Command | Reads |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `context`, `rulings` | `--brief`, `--base`, `--root`, `--format` |
|
|
385
|
+
| `guard`, `audit`, `doctor` | `--brief`, `--base`, `--root`, `--format`, `--strict` |
|
|
386
|
+
| `probe` | `--brief`, `--base`, `--root`, `--format`, `--id`, `--at` |
|
|
387
|
+
| `premises` | `--brief`, `--root`, `--format`, `--strict` |
|
|
388
|
+
| `escalate` | `--brief`, `--root`, `--format`, `--path`, `--reason`, `--option`, `--recommend`, `--list`, `--show` |
|
|
389
|
+
| `rule` | `--root`, `--format`, `--allow`, `--deny`, `--note` |
|
|
390
|
+
| `init` | `--root`, `--format`, `--write`, `--git-hook` |
|
|
391
|
+
| `hook` | `--brief`, `--base`, `--root` |
|
|
392
|
+
| `mcp` | `--root` |
|
|
393
|
+
|
|
394
|
+
`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.
|
|
395
|
+
- **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.
|
|
396
|
+
- **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.
|
|
397
|
+
- **A `--root` that is no directory**, a file or a path that is not there: `--root is "nowhere", which is not a directory`.
|
|
398
|
+
- **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.
|
|
399
|
+
|
|
400
|
+
`--help` and `--version` answer whatever else is on the line. Three things are read another way, each for its reason:
|
|
401
|
+
|
|
402
|
+
- **`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)).
|
|
403
|
+
- **`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.
|
|
404
|
+
- **`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)).
|
|
405
|
+
|
|
406
|
+
**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.
|
|
407
|
+
|
|
353
408
|
## As a Claude Code plugin
|
|
354
409
|
|
|
355
410
|
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)).
|
|
@@ -425,7 +480,7 @@ A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-har
|
|
|
425
480
|
| --- | --- |
|
|
426
481
|
| `0` | Clean. |
|
|
427
482
|
| `1` | Refused, found something, or waiting on a person. |
|
|
428
|
-
| `2` | The answer cannot be trusted. An error the harness did not expect is one: `spec-harness: unexpected error:` and its stack, on stderr. |
|
|
483
|
+
| `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. |
|
|
429
484
|
| `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
|
|
430
485
|
| `143` | A `probe` interrupted by SIGTERM. |
|
|
431
486
|
| `129` | A `probe` interrupted by SIGHUP. |
|
package/bin/spec-harness.js
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
// main answers the errors it awaits with exit 2. What nothing awaits - a
|
|
8
8
|
// stream's error, a timer's, a promise nobody holds - Node ends with exit 1,
|
|
9
9
|
// which a script reads as a refusal or a finding: the same answer for those.
|
|
10
|
+
//
|
|
11
|
+
// A reader that closed stdout, as `| head` does, is no defect, and it arrives
|
|
12
|
+
// here, as the stream's error: one line, with no stack to send a person
|
|
13
|
+
// looking for a bug. The code alone names it, since stdout and stderr are the
|
|
14
|
+
// only pipes the harness writes to, and a line about a closed stderr reaches
|
|
15
|
+
// nobody.
|
|
10
16
|
process.on('uncaughtException', (error) => {
|
|
11
|
-
|
|
17
|
+
const said = error?.code === 'EPIPE' ? 'stdout was closed before all of the output was written' : `unexpected error: ${error?.stack ?? error}`;
|
|
18
|
+
process.stderr.write(`spec-harness: ${said}\n`);
|
|
12
19
|
process.exit(2);
|
|
13
20
|
});
|
|
14
21
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,14 +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,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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).
|
|
9
11
|
*/
|
|
10
12
|
import { type CliIO } from './workspace.js';
|
|
11
13
|
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
|
|
12
|
-
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
|
|
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.\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, and an option or an\nargument given an empty value are refused, exit 2, not read as if they were not there.\ndoctor says an empty --brief or --base in its report, and hook claude reads its line\nas every release has.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
|
|
13
15
|
/**
|
|
14
16
|
* Runs the command line and resolves to the exit code, for an error nothing
|
|
15
17
|
* below expected too.
|
|
@@ -21,6 +23,14 @@ export declare const HELP = "spec-harness - context, guards, audits and rulings
|
|
|
21
23
|
* write the guard could not check went ahead. The stack is what makes a
|
|
22
24
|
* report of it something to act on, and it goes to stderr alone: a script
|
|
23
25
|
* that reads a document from stdout is handed no part of one.
|
|
26
|
+
*
|
|
27
|
+
* A write its reader had closed the pipe for is not such an error: the answer
|
|
28
|
+
* was not delivered, which is still 2, and nothing in the harness is at
|
|
29
|
+
* fault, so it is said in a line and no stack sends a person looking for a
|
|
30
|
+
* defect. The process's own streams report it as an event, which the
|
|
31
|
+
* launcher answers in the same words; here it is a caller's stream that
|
|
32
|
+
* throws it. It is read by its code: stdout and stderr are the only pipes
|
|
33
|
+
* the harness writes to.
|
|
24
34
|
*/
|
|
25
35
|
export declare function run(argv: readonly string[], io: CliIO): Promise<number>;
|
|
26
36
|
export declare function main(argv?: readonly string[]): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
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,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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).
|
|
9
11
|
*/
|
|
10
12
|
import { SiblingOutputError } from './briefs.js';
|
|
11
13
|
import { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';
|
|
12
14
|
import { ConfigError, SIBLINGS } from './config.js';
|
|
13
15
|
import { describeBase, describeClaudeCode, describeClaudeRelease, describeGitHook, describePlugin, describeSignerKeys, describeSignerProblem, describeSigners, wiringState, } from './configure.js';
|
|
14
|
-
import {
|
|
16
|
+
import { isDirectory } from './fs.js';
|
|
17
|
+
import { GitError, show, stagedChanges } from './git.js';
|
|
15
18
|
import { claudeResponse, gitResponse, parseClaudeHook } from './hooks.js';
|
|
16
19
|
import { claudeVersion } from './host.js';
|
|
17
20
|
import { premisesCommand } from './premises.js';
|
|
@@ -21,6 +24,7 @@ import { mcpCommand } from './server.js';
|
|
|
21
24
|
import { gitHook, initCommand } from './setup.js';
|
|
22
25
|
import { notFido2, readAllowedSigners } from './signers.js';
|
|
23
26
|
import { SiblingError } from './siblings.js';
|
|
27
|
+
import { emptyValue, isBlank, refusal } from './usage.js';
|
|
24
28
|
import { checkClaudeCode, CLAUDE_CODE_MINIMUM, MINIMUM_VERSIONS } from './versions.js';
|
|
25
29
|
import { activeBrief, describeActive, EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, namedId, openWorkspace, parseOptions, UsageError, version, } from './workspace.js';
|
|
26
30
|
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './workspace.js';
|
|
@@ -39,13 +43,13 @@ Commands:
|
|
|
39
43
|
[--option "<label>: <cost>"]... [--recommend <text>].
|
|
40
44
|
--list shows what waits; --show <id> prints one memo.
|
|
41
45
|
rule <id> Record a person's ruling in the brief: --allow or --deny, with
|
|
42
|
-
--note
|
|
46
|
+
--note <text>. It counts once committed signed.
|
|
43
47
|
rulings [brief] The brief's rulings, and whether each one's signature verifies.
|
|
44
48
|
audit [brief] Did the round stay inside the lines: what the archive would say,
|
|
45
49
|
the brief's assertions, unverified rulings, new dependencies
|
|
46
50
|
and the install scripts it allowed.
|
|
47
51
|
probe [brief] Run the brief's probes: red at the base, green at the head.
|
|
48
|
-
--at base|head|both
|
|
52
|
+
--at <base|head|both>, --id <probe>.
|
|
49
53
|
premises Do the live briefs' premises still hold? For CI: exit 1 when one
|
|
50
54
|
is stale, or, with --strict, when spec-guard cannot read one.
|
|
51
55
|
init Configure the spec-* tools to agree. Prints the plan; --write
|
|
@@ -59,13 +63,22 @@ Commands:
|
|
|
59
63
|
|
|
60
64
|
Options:
|
|
61
65
|
--brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.
|
|
66
|
+
For context, guard, hook, escalate, rulings, audit, probe, premises
|
|
67
|
+
and doctor.
|
|
62
68
|
--base <ref> What the round is measured from. Otherwise "base" in
|
|
63
|
-
.spec-harness.json, then the remote's default branch.
|
|
64
|
-
|
|
69
|
+
.spec-harness.json, then the remote's default branch. For context,
|
|
70
|
+
guard, hook, rulings, audit, probe and doctor.
|
|
71
|
+
--root <dir> Run from another directory, which must exist. For every command.
|
|
65
72
|
--format <fmt> pretty or json; audit and premises also gitlab, sarif or github.
|
|
66
|
-
|
|
73
|
+
For every command but hook and mcp.
|
|
74
|
+
--strict Warnings fail the run. For guard, audit, premises and doctor.
|
|
67
75
|
--help, --version
|
|
68
76
|
|
|
77
|
+
An option a command does not read, an argument it does not take, and an option or an
|
|
78
|
+
argument given an empty value are refused, exit 2, not read as if they were not there.
|
|
79
|
+
doctor says an empty --brief or --base in its report, and hook claude reads its line
|
|
80
|
+
as every release has.
|
|
81
|
+
|
|
69
82
|
Exit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.
|
|
70
83
|
`;
|
|
71
84
|
/* ------------------------------------------------------------------- guard */
|
|
@@ -184,6 +197,32 @@ async function hookCommand(options, io) {
|
|
|
184
197
|
throw new UsageError('hook takes "claude" or "git"');
|
|
185
198
|
}
|
|
186
199
|
/* ------------------------------------------------------------------ doctor */
|
|
200
|
+
/**
|
|
201
|
+
* The brief a round here works on, as doctor reports it: its id, and what is
|
|
202
|
+
* wrong with the name when no round can work under it. Every other command
|
|
203
|
+
* stops there. A diagnosis says it, being what a person runs when the guard
|
|
204
|
+
* holds a write they expected it to pass.
|
|
205
|
+
*/
|
|
206
|
+
async function diagnoseBrief(workspace, options, env) {
|
|
207
|
+
if (options.brief !== undefined && isBlank(options.brief))
|
|
208
|
+
return { id: null, problem: emptyValue('--brief', 'brief', options.brief) };
|
|
209
|
+
const id = namedId(workspace, options, env);
|
|
210
|
+
// A spec-brief that cannot be run is a row of the report, with the reason.
|
|
211
|
+
if (workspace.siblings.locate('spec-brief').kind !== 'found')
|
|
212
|
+
return { id, problem: null };
|
|
213
|
+
try {
|
|
214
|
+
// Where nothing names a brief, spec-brief is not asked (ADR-0004), and
|
|
215
|
+
// there is no problem to state.
|
|
216
|
+
return { id, problem: describeActive(await activeBrief(workspace, options, env)).problem };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Asking spec-brief throws nothing but this, so the check is equivalent
|
|
220
|
+
// to its mutant; it keeps any other error the stack it needs.
|
|
221
|
+
if (!(error instanceof SiblingError))
|
|
222
|
+
throw error;
|
|
223
|
+
return { id, problem: `spec-brief could not say whether it knows brief ${id}: ${error.message}` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
187
226
|
async function doctorCommand(options, io) {
|
|
188
227
|
const workspace = await openWorkspace(options, io);
|
|
189
228
|
const rows = SIBLINGS.map((name) => {
|
|
@@ -197,10 +236,15 @@ async function doctorCommand(options, io) {
|
|
|
197
236
|
const detail = sibling.version === null ? `${command} (named in .spec-harness.json; its version is not checked)` : `${command} (${sibling.version})`;
|
|
198
237
|
return { ...row, version: sibling.version, detail };
|
|
199
238
|
});
|
|
200
|
-
const named =
|
|
239
|
+
const named = await diagnoseBrief(workspace, options, io.env);
|
|
201
240
|
// A ruling counts only by a signature checked against the allowed signers
|
|
202
241
|
// on the base, and only once spec-brief's archive asks the plugin about it.
|
|
203
|
-
|
|
242
|
+
// A --base given nothing is no base, said as the brief's is, in the report.
|
|
243
|
+
// Nothing below asks what kind a base is but whether it is resolved, so the
|
|
244
|
+
// mutant that names this one otherwise is equivalent.
|
|
245
|
+
const base = options.base !== undefined && isBlank(options.base)
|
|
246
|
+
? { kind: 'unresolved', reason: emptyValue('--base', 'base', options.base) }
|
|
247
|
+
: await resolveBase(workspace, options.base);
|
|
204
248
|
const signersFile = workspace.config.rulings.allowedSigners;
|
|
205
249
|
// An unresolved base has no commit: git shows nothing for one, so asking
|
|
206
250
|
// anyway reads as no file, and that mutant is equivalent.
|
|
@@ -229,7 +273,8 @@ async function doctorCommand(options, io) {
|
|
|
229
273
|
root: workspace.root,
|
|
230
274
|
branch: workspace.branch,
|
|
231
275
|
branchSource: workspace.branchSource ?? null,
|
|
232
|
-
brief: named,
|
|
276
|
+
brief: named.id,
|
|
277
|
+
briefProblem: named.problem,
|
|
233
278
|
base: base.kind === 'resolved' ? { ref: base.ref, source: base.source, mergeBase: base.mergeBase } : { unresolved: base.reason },
|
|
234
279
|
allowedSigners: {
|
|
235
280
|
file: signersFile,
|
|
@@ -256,7 +301,7 @@ async function doctorCommand(options, io) {
|
|
|
256
301
|
io.stdout.write([
|
|
257
302
|
`root ${workspace.root}`,
|
|
258
303
|
`branch ${branch}`,
|
|
259
|
-
`brief ${named ?? '(none named)'}`,
|
|
304
|
+
`brief ${named.problem === null ? (named.id ?? '(none named)') : `none: ${named.problem}`}`,
|
|
260
305
|
`base ${describeBase(base)}`,
|
|
261
306
|
`signers ${signers}`,
|
|
262
307
|
...more(signerNotes),
|
|
@@ -289,20 +334,22 @@ async function doctorCommand(options, io) {
|
|
|
289
334
|
* `Object` as a command and ended on a stack trace, where `spec-harness
|
|
290
335
|
* construct` is an unknown command with exit 2.
|
|
291
336
|
*/
|
|
292
|
-
const COMMANDS = new Map(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
337
|
+
const COMMANDS = new Map(
|
|
338
|
+
// One for every command that has a shape (usage.ts), which the compiler holds.
|
|
339
|
+
Object.entries({
|
|
340
|
+
guard: guardCommand,
|
|
341
|
+
hook: hookCommand,
|
|
342
|
+
context: contextCommand,
|
|
343
|
+
audit: auditCommand,
|
|
344
|
+
escalate: escalateCommand,
|
|
345
|
+
rule: ruleCommand,
|
|
346
|
+
rulings: rulingsCommand,
|
|
347
|
+
probe: probeCommand,
|
|
348
|
+
premises: premisesCommand,
|
|
349
|
+
init: initCommand,
|
|
350
|
+
mcp: mcpCommand,
|
|
351
|
+
doctor: doctorCommand,
|
|
352
|
+
}));
|
|
306
353
|
async function dispatch(argv, io) {
|
|
307
354
|
let options;
|
|
308
355
|
try {
|
|
@@ -325,6 +372,12 @@ async function dispatch(argv, io) {
|
|
|
325
372
|
io.stderr.write(`spec-harness: unknown command "${options.command}"; see spec-harness --help\n`);
|
|
326
373
|
return EXIT_ERROR;
|
|
327
374
|
}
|
|
375
|
+
// What parsed and names nothing, before anything is read or written for it.
|
|
376
|
+
const refused = refusal(options, isDirectory);
|
|
377
|
+
if (refused !== null) {
|
|
378
|
+
io.stderr.write(`spec-harness: ${refused}\n`);
|
|
379
|
+
return EXIT_ERROR;
|
|
380
|
+
}
|
|
328
381
|
return command(options, io);
|
|
329
382
|
}
|
|
330
383
|
/**
|
|
@@ -338,6 +391,14 @@ async function dispatch(argv, io) {
|
|
|
338
391
|
* write the guard could not check went ahead. The stack is what makes a
|
|
339
392
|
* report of it something to act on, and it goes to stderr alone: a script
|
|
340
393
|
* that reads a document from stdout is handed no part of one.
|
|
394
|
+
*
|
|
395
|
+
* A write its reader had closed the pipe for is not such an error: the answer
|
|
396
|
+
* was not delivered, which is still 2, and nothing in the harness is at
|
|
397
|
+
* fault, so it is said in a line and no stack sends a person looking for a
|
|
398
|
+
* defect. The process's own streams report it as an event, which the
|
|
399
|
+
* launcher answers in the same words; here it is a caller's stream that
|
|
400
|
+
* throws it. It is read by its code: stdout and stderr are the only pipes
|
|
401
|
+
* the harness writes to.
|
|
341
402
|
*/
|
|
342
403
|
export async function run(argv, io) {
|
|
343
404
|
try {
|
|
@@ -345,12 +406,22 @@ export async function run(argv, io) {
|
|
|
345
406
|
}
|
|
346
407
|
catch (error) {
|
|
347
408
|
// A sibling's document of a shape the harness does not read is a sibling
|
|
348
|
-
// that printed something it cannot read (answers.ts): exit 2.
|
|
349
|
-
|
|
409
|
+
// that printed something it cannot read (answers.ts): exit 2. A git
|
|
410
|
+
// command that failed is what git said, and no line of the harness's.
|
|
411
|
+
if (error instanceof UsageError ||
|
|
412
|
+
error instanceof ConfigError ||
|
|
413
|
+
error instanceof SiblingError ||
|
|
414
|
+
error instanceof SiblingOutputError ||
|
|
415
|
+
error instanceof GitError) {
|
|
350
416
|
io.stderr.write(`spec-harness: ${error.message}\n`);
|
|
351
417
|
return EXIT_ERROR;
|
|
352
418
|
}
|
|
353
|
-
|
|
419
|
+
if (!(error instanceof Error))
|
|
420
|
+
io.stderr.write(`spec-harness: unexpected error: ${String(error)}\n`);
|
|
421
|
+
else if (error.code === 'EPIPE')
|
|
422
|
+
io.stderr.write('spec-harness: stdout was closed before all of the output was written\n');
|
|
423
|
+
else
|
|
424
|
+
io.stderr.write(`spec-harness: unexpected error: ${error.stack ?? error.message}\n`);
|
|
354
425
|
return EXIT_ERROR;
|
|
355
426
|
}
|
|
356
427
|
}
|