@descent-vtt/spec-harness 0.12.0 → 0.14.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 +96 -0
- package/README.md +23 -5
- package/dist/answers.d.ts +6 -0
- package/dist/answers.js +10 -0
- package/dist/answers.js.map +1 -1
- package/dist/briefs.d.ts +10 -5
- package/dist/briefs.js +5 -3
- package/dist/briefs.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +78 -26
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +5 -1
- package/dist/commands.js +7 -2
- package/dist/commands.js.map +1 -1
- package/dist/context.d.ts +16 -7
- package/dist/context.js +38 -11
- package/dist/context.js.map +1 -1
- package/dist/fs.js +1 -1
- package/dist/fs.js.map +1 -1
- package/dist/premises.js +11 -1
- package/dist/premises.js.map +1 -1
- package/dist/round.js +3 -1
- package/dist/round.js.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/usage.d.ts +28 -4
- package/dist/usage.js +47 -16
- package/dist/usage.js.map +1 -1
- package/dist/workspace.d.ts +16 -2
- package/dist/workspace.js +62 -35
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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,102 @@ 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.14.0
|
|
13
|
+
|
|
14
|
+
`hook claude` says a line of its own that it cannot read with exit 1 and
|
|
15
|
+
holds no write for it, where exit 2 held every write of the session. And
|
|
16
|
+
where spec-guard refuses for a spec pattern that matched no file, the
|
|
17
|
+
context packet lists the rules of the files that did match, under a
|
|
18
|
+
sentence that names the pattern, where it listed none.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`hook claude` says a line it cannot read, and exits 1.** An option no
|
|
23
|
+
command has, an option left without its value, and a `--format` or an
|
|
24
|
+
`--at` that is none were exit 2 from Claude Code's hook as from every
|
|
25
|
+
command, and Claude Code holds a write on a PreToolUse hook's exit 2:
|
|
26
|
+
every write of the session, the one that mends the line among them. It is
|
|
27
|
+
now one line on stderr, `the hook's line cannot be read (...), so the
|
|
28
|
+
guard is off and this write is not checked`, with the parser's words
|
|
29
|
+
between the brackets, and exit 1, which Claude Code shows as a hook error
|
|
30
|
+
while the write goes on, as for a `--root` that is not a directory. Exit 2
|
|
31
|
+
from the hook is for a write it could not check, and `hook git` and every
|
|
32
|
+
other command stop on such a line as before
|
|
33
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
34
|
+
Upgrading: no release wrote more than `hook claude` on the hook's line. If
|
|
35
|
+
yours holds more and cannot be read, it held every write and now holds
|
|
36
|
+
none: mend the line the hook error names. A hook entry that sets
|
|
37
|
+
`"onFailure": "block"` has Claude Code hold the write for exit 1 too.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **The context packet lists the rules of the rest where spec-guard refuses
|
|
42
|
+
for a spec pattern that matched no file.** spec-guard 0.22.0 exits 2 for
|
|
43
|
+
a pattern among its `specs` that matches no file beside others that do,
|
|
44
|
+
and still answers with the rules of the files that matched. `context` and
|
|
45
|
+
`start_round` read that as rules that could not be read and listed none.
|
|
46
|
+
They now list those rules under one sentence that names the pattern and
|
|
47
|
+
says a rule kept under it is not listed, and name the patterns in
|
|
48
|
+
`--format json` and in `start_round` as `unmatchedSpecs`, an empty list
|
|
49
|
+
where there is none. Any other refusal reads as it did, and so does a
|
|
50
|
+
spec-guard older than 0.22.0, which writes no such field
|
|
51
|
+
([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)).
|
|
52
|
+
|
|
53
|
+
## 0.13.0
|
|
54
|
+
|
|
55
|
+
`hook claude` says a `--root` on its line that is not a directory, with
|
|
56
|
+
exit 1 and no write refused, where the guard was off and nothing said so.
|
|
57
|
+
And four more inputs that named nothing, or were not read, are refused by
|
|
58
|
+
name: a brief `premises` is given that spec-brief does not know, an option
|
|
59
|
+
`escalate --list` or `--show` leaves unread, and a brief given as the
|
|
60
|
+
argument and as `--brief` at once.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **`hook claude` says a `--root` on its line that is not a directory, and
|
|
65
|
+
exits 1.** It exited 0 with no word, as outside a git work tree, and the
|
|
66
|
+
write went on unguarded; it is now one line on stderr that names the root,
|
|
67
|
+
and exit 1, which Claude Code shows as a hook error without blocking the
|
|
68
|
+
write
|
|
69
|
+
([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
70
|
+
Upgrading: no release wrote a `--root` on the hook's line; if you added
|
|
71
|
+
one, point it at a directory that exists, or take it off.
|
|
72
|
+
- **`premises` refuses a brief `--brief` or `SPEC_BRIEF` names that
|
|
73
|
+
spec-brief does not know**, as every other command does: `premises
|
|
74
|
+
--brief 999` was read as no round, and held the round's own premise
|
|
75
|
+
against it. It is exit 2 and `the flag names brief 999, and spec-brief
|
|
76
|
+
knows no such brief`
|
|
77
|
+
([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
|
|
78
|
+
Upgrading: name a brief spec-brief lists, or none. A branch whose name
|
|
79
|
+
reads as a brief's and names none is read as no round, as before.
|
|
80
|
+
- **`escalate --list` and `--show` refuse an option they do not read.**
|
|
81
|
+
Beside either, `--path`, `--reason`, `--option`, `--recommend` and
|
|
82
|
+
`--brief` did nothing, and `--show` did nothing beside `--list`: the list
|
|
83
|
+
was printed, exit 0, and a request spelled out beside it was never raised
|
|
84
|
+
([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
|
|
85
|
+
Upgrading: give `--list` or `--show` with `--root` and `--format` alone,
|
|
86
|
+
and raise a request in a call of its own.
|
|
87
|
+
- **A brief given as the argument and as `--brief` at once is refused**,
|
|
88
|
+
whatever the two hold, by `context`, `rulings`, `audit` and `probe`:
|
|
89
|
+
`audit 012 --brief 013` audited 012 with no word about 013. Upgrading:
|
|
90
|
+
give one of them. `SPEC_BRIEF` and the branch beside either are read as
|
|
91
|
+
before.
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- `findActive`, from the package, takes the brief an `argument` names, which
|
|
96
|
+
comes before the flag, and gives `argument` as its `source`.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- A brief that is not one is said by what named it: `the argument names
|
|
101
|
+
brief 999` for `context 999`, and `"brief" names brief 999` from the
|
|
102
|
+
server's tools, where both said `the flag`.
|
|
103
|
+
- The context packet ends once the sentence in which it quotes why
|
|
104
|
+
spec-guard could not be asked: a refusal spec-guard ends with a full stop
|
|
105
|
+
of its own, as each has since its 0.21.0, read `... in package.json..
|
|
106
|
+
Treat every ADR as binding`.
|
|
107
|
+
|
|
12
108
|
## 0.12.0
|
|
13
109
|
|
|
14
110
|
A probe's command is answered when its shell ends, by the shell's exit code,
|
package/README.md
CHANGED
|
@@ -74,7 +74,8 @@ A name that names no brief is not passed over for the next ([ADR-0004](https://g
|
|
|
74
74
|
|
|
75
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
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.
|
|
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.
|
|
78
79
|
|
|
79
80
|
## Commands
|
|
80
81
|
|
|
@@ -100,6 +101,8 @@ The packet an agent starts a round with: the brief in full - it is the contract
|
|
|
100
101
|
|
|
101
102
|
Rules spec-guard could not read are named as unread, with what it said, never as none; a repository where no spec file matches spec-guard's patterns has none.
|
|
102
103
|
|
|
104
|
+
A spec pattern of spec-guard's that matches no file beside others that do is neither. spec-guard 0.22.0 and later refuse the question for it, exit 2, and still answer with the rules of every spec file that did match. The packet lists those rules as it lists any, under one sentence that names the pattern: `` spec-guard's pattern `rfcs/*.md` matched no spec file, so a rule kept under it is not listed here; remove the pattern from spec-guard's `specs`. `` `context --format json` and `start_round` name the patterns as `unmatchedSpecs`, an empty list where spec-guard names none. An older spec-guard says nothing of such a pattern, and its rules are listed as they were.
|
|
105
|
+
|
|
103
106
|
**What it could not read is named, never left out.** A cited document whose front matter opens on its first line and is never closed is one spec-core's scanner reads as having no front matter: its status was not read, and the packet says so, with the fix - close the block with `---` on a line of its own - rather than showing a document without one. So is each line of closed front matter that is not `key: value`, such as `status accepted` without its colon, or an indented line with no key above it: spec-core's reader passes over it, so a status written on it was not read, and the packet names the document, the line as written and spec-core's reason. `context --format json` and `start_round` list them as `unreadableFrontMatter`, each with its `path`, `line`, `text` and `reason`. The notes fail nothing; `context` exits 0.
|
|
104
107
|
|
|
105
108
|
A pattern the guard cannot read - malformed, too large to compile, or naming no path, such as `{./,src}` - is named as unread where it is listed, with spec-core's reason, the one the guard gives when it passes over it:
|
|
@@ -131,6 +134,8 @@ Paths are compared as the filesystem spells them, links resolved and case correc
|
|
|
131
134
|
- `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.
|
|
132
135
|
- `spec-harness hook git` is a pre-commit hook for any agent or none. `init --git-hook` writes it; in a hook of your own it is the line `node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git`.
|
|
133
136
|
- `hook claude` passes over whatever else is on its line that it does not read, as every release has, where every other command [refuses it](#an-input-that-names-nothing): its line is in settings a later release cannot rewrite, and exit 2 from it holds every write.
|
|
137
|
+
- A `--root` on the hook's line that is a file, or is not there, is a guard that was set up and is off: `hook claude` says so on stderr and exits 1, which Claude Code shows as a hook error while the write goes on. It refuses nothing for it. No release wrote a `--root` there; without one the hook reads the project Claude Code names, then the session's directory, and outside a git work tree it has nothing to guard and says nothing.
|
|
138
|
+
- A line the hook cannot read - an option no command has, an option left without its value, a `--format` or an `--at` that is none - is said the same way: one line on stderr, exit 1, and no write held for it. Exit 2 from `hook claude` is for a write it could not check, never for what is wrong with its own line. `hook git` stops the commit on such a line, exit 2, where a person reads why.
|
|
134
139
|
|
|
135
140
|
A guard is a guardrail - an agent that writes through a shell passes it - so the audit and spec-brief's archive are the gates ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
136
141
|
|
|
@@ -151,6 +156,8 @@ git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
|
|
|
151
156
|
|
|
152
157
|
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
158
|
|
|
159
|
+
`escalate --list` prints every request that waits, whichever round raised it, and `escalate --show <id>` one memo. Neither raises a request, so neither reads `--path`, `--reason`, `--option`, `--recommend` or `--brief`, and either beside one of them is [refused](#an-input-that-names-nothing): the request was never raised, and the list looked like an answer to it.
|
|
160
|
+
|
|
154
161
|
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.
|
|
155
162
|
|
|
156
163
|
- 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)).
|
|
@@ -307,8 +314,10 @@ Is every live brief still about something true? It runs spec-guard over the live
|
|
|
307
314
|
- Goals are left to `audit`, since a goal fails until its round is done.
|
|
308
315
|
- 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.
|
|
309
316
|
- 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.
|
|
317
|
+
- 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.
|
|
318
|
+
- 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.
|
|
310
319
|
|
|
311
|
-
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.
|
|
320
|
+
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.
|
|
312
321
|
|
|
313
322
|
### `mcp`
|
|
314
323
|
|
|
@@ -392,6 +401,8 @@ An option, an argument or a value that is given and names nothing is refused: ex
|
|
|
392
401
|
| `mcp` | `--root` |
|
|
393
402
|
|
|
394
403
|
`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.
|
|
404
|
+
- **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.
|
|
405
|
+
- **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.
|
|
395
406
|
- **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
407
|
- **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
408
|
- **A `--root` that is no directory**, a file or a path that is not there: `--root is "nowhere", which is not a directory`.
|
|
@@ -399,11 +410,18 @@ An option, an argument or a value that is given and names nothing is refused: ex
|
|
|
399
410
|
|
|
400
411
|
`--help` and `--version` answer whatever else is on the line. Three things are read another way, each for its reason:
|
|
401
412
|
|
|
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
|
|
413
|
+
- **`hook claude` reads its line as every release has.** The line is in `.claude/settings.json` and in the Claude Code plugin of every release since 0.1.0, which a later release cannot rewrite, and exit 2 from it holds every write of a session, the one that would mend the line among them. So what it does not read is passed over as before, an empty `--brief` with the rest. The releases have handed the harness five command lines, `hook claude`, `hook git`, `mcp`, and `mcp --root` with the project or with `.`, and each is read as the release that wrote it read it ([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
|
|
414
|
+
|
|
415
|
+
Two things on its line are said, and still refuse nothing. Each is one line on stderr and exit 1, said where there is a write the line leaves unchecked. Claude Code does not block a tool call on exit 1: it goes on with the write and shows the line as a hook error in the transcript. No release wrote more than `hook claude` on the hook's line, so either is a line a person changed.
|
|
416
|
+
|
|
417
|
+
- **A `--root` that is a file, or is not there.** It read as a session outside a git work tree, exit 0 and no word, and the guard was off. It is `--root is "nowhere", which is not a directory, so the guard is off and this write is not checked`, with what to do. An empty `--root` is the directory the hook was started in, and guards there, as before.
|
|
418
|
+
- **A line the parser cannot read**: an option no command has, as in `hook claude --nope`; an option left without its value, as `--root $DIR` is in a shell where the variable is not set; a `--format` or an `--at` that is none. Each was exit 2, as it is from every other command, and Claude Code blocks the tool call on a PreToolUse hook's exit 2: every write of the session waited on a line only a write can mend. It is `the hook's line cannot be read (Option '--root <value>' argument missing), so the guard is off and this write is not checked`, with what to do. The parser's own words stand between the brackets, on one line.
|
|
419
|
+
|
|
420
|
+
Exit 2 from `hook claude` is kept for a write it could not check: a configuration that does not load, briefs that cannot be read, a brief that is named and is not one, an error nothing expected. `hook git` is not read this way. It stops one commit, exit 2, on a line a person reads, who can then mend the hook. Nor is a line that does not say `hook claude`, such as `hook` alone: the harness cannot tell whose it is. A hook entry that sets `"onFailure": "block"`, which Claude Code 2.1.295 and later read, has Claude Code hold the action when the hook fails, and exit 1 is a failure to it; neither `init` nor the plugin writes that field.
|
|
403
421
|
- **`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
422
|
- **`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
423
|
|
|
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.
|
|
424
|
+
**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.
|
|
407
425
|
|
|
408
426
|
## As a Claude Code plugin
|
|
409
427
|
|
|
@@ -479,7 +497,7 @@ A person who runs the tools by hand runs `npx --no-install @descent-vtt/spec-har
|
|
|
479
497
|
| Code | Means |
|
|
480
498
|
| --- | --- |
|
|
481
499
|
| `0` | Clean. |
|
|
482
|
-
| `1` | Refused, found something, or waiting on a person. |
|
|
500
|
+
| `1` | Refused, found something, or waiting on a person. From `hook claude`, also what it says and holds no write for: a question it cannot read, and a line of its own it cannot read or whose `--root` is no directory. |
|
|
483
501
|
| `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. |
|
|
484
502
|
| `130` | A `probe` interrupted by SIGINT (Ctrl+C) while it runs. |
|
|
485
503
|
| `143` | A `probe` interrupted by SIGTERM. |
|
package/dist/answers.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface QueryRule {
|
|
|
26
26
|
}
|
|
27
27
|
/** The rules in each of the results of `spec-guard query --json`, in order. */
|
|
28
28
|
export declare function readQueryRules(document: unknown): QueryRule[][];
|
|
29
|
+
/**
|
|
30
|
+
* The spec patterns `spec-guard query --json` says matched no file, as
|
|
31
|
+
* written. A document that says nothing of them names none: spec-guard has
|
|
32
|
+
* written the field since 0.22.0, and only where there is such a pattern.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readQueryUnmatched(document: unknown): string[];
|
|
29
35
|
export interface GuardResult {
|
|
30
36
|
readonly ok: boolean;
|
|
31
37
|
readonly description: string;
|
package/dist/answers.js
CHANGED
|
@@ -70,6 +70,16 @@ export function readQueryRules(document) {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The spec patterns `spec-guard query --json` says matched no file, as
|
|
75
|
+
* written. A document that says nothing of them names none: spec-guard has
|
|
76
|
+
* written the field since 0.22.0, and only where there is such a pattern.
|
|
77
|
+
*/
|
|
78
|
+
export function readQueryUnmatched(document) {
|
|
79
|
+
const read = reader('spec-guard query');
|
|
80
|
+
const top = read.object(document, 'a document');
|
|
81
|
+
return read.list(top['unmatchedSpecs'], 'unmatchedSpecs').map((value, index) => read.text(value, `unmatchedSpecs[${index}]`));
|
|
82
|
+
}
|
|
73
83
|
/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */
|
|
74
84
|
export function readGuardRun(document) {
|
|
75
85
|
const read = reader('spec-guard');
|
package/dist/answers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What spec-guard and spec-brief answer in JSON, besides the brief list\n * briefs.ts reads: a query's rules, a run's results and errors, and an\n * archive's plan. Each is checked for the fields the harness reads, with the\n * types it reads them as, and a document of another shape is an error that\n * names the tool and the field (ADR-0002): the command that asked stops with\n * exit 2, as for any sibling that printed something it cannot read, rather\n * than crash halfway through the document or count what it could not read as\n * clean. Fields the harness does not read are not checked, so a sibling may\n * add to its documents freely.\n */\n\nimport type { ArchiveReason } from './audit.js';\nimport { SiblingOutputError } from './briefs.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads fields of one document, naming the tool, its command and the field in every refusal. */\nfunction reader(printer: string) {\n const refuse = (where: string, expected: string): never => {\n throw new SiblingOutputError(`${printer} printed ${where} that is not ${expected}`);\n };\n return {\n object(value: unknown, where: string): Json {\n return isObject(value) ? value : refuse(where, 'an object');\n },\n /** A list that is absent is an empty one. */\n list(value: unknown, where: string): unknown[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? (value as unknown[]) : refuse(where, 'a list');\n },\n text(value: unknown, where: string): string {\n return typeof value === 'string' ? value : refuse(where, 'text');\n },\n optionalText(value: unknown, where: string): string | undefined {\n return value === undefined ? undefined : typeof value === 'string' ? value : refuse(where, 'text');\n },\n number(value: unknown, where: string): number {\n return typeof value === 'number' ? value : refuse(where, 'a number');\n },\n optionalNumber(value: unknown, where: string): number | undefined {\n return value === undefined ? undefined : typeof value === 'number' ? value : refuse(where, 'a number');\n },\n flag(value: unknown, where: string): boolean {\n return typeof value === 'boolean' ? value : refuse(where, 'true or false');\n },\n refuse,\n };\n}\n\n/** Where spec-guard places a directive. */\nexport interface Spec {\n readonly file: string;\n readonly line: number;\n}\n\nexport interface QueryRule {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n /** Absent from a document that says nothing of it, which is in force. */\n readonly inForce: boolean;\n}\n\n/** The rules in each of the results of `spec-guard query --json`, in order. */\nexport function readQueryRules(document: unknown): QueryRule[][] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['results'], 'results').map((value, index) => {\n const result = read.object(value, `results[${index}]`);\n return read.list(result['rules'], `results[${index}].rules`).map((ruleValue, at) => {\n const where = `results[${index}].rules[${at}]`;\n const rule = read.object(ruleValue, where);\n const reason = rule['reason'];\n if (reason !== undefined && reason !== null && typeof reason !== 'string') read.refuse(`${where}.reason`, 'text or null');\n const inForce = rule['inForce'];\n return {\n document: read.text(rule['document'], `${where}.document`),\n line: read.number(rule['line'], `${where}.line`),\n kind: read.text(rule['kind'], `${where}.kind`),\n description: read.text(rule['description'], `${where}.description`),\n reason: typeof reason === 'string' ? reason : null,\n inForce: inForce === undefined ? true : read.flag(inForce, `${where}.inForce`),\n };\n });\n });\n}\n\nexport interface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec | undefined;\n}\n\nexport interface GuardError {\n readonly message: string;\n readonly raw: string;\n readonly spec?: Spec | undefined;\n}\n\n/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */\nexport function readGuardRun(document: unknown): { results: GuardResult[]; errors: GuardError[] } {\n const read = reader('spec-guard');\n const top = read.object(document, 'a document');\n const spec = (value: unknown, where: string): Spec | undefined => {\n if (value === undefined) return undefined;\n const placed = read.object(value, where);\n return { file: read.text(placed['file'], `${where}.file`), line: read.number(placed['line'], `${where}.line`) };\n };\n const results = read.list(top['results'], 'results').map((value, index) => {\n const where = `results[${index}]`;\n const result = read.object(value, where);\n return {\n ok: read.flag(result['ok'], `${where}.ok`),\n description: read.text(result['description'], `${where}.description`),\n message: read.text(result['message'], `${where}.message`),\n spec: spec(result['spec'], `${where}.spec`),\n };\n });\n const errors = read.list(top['errors'], 'errors').map((value, index) => {\n const where = `errors[${index}]`;\n const error = read.object(value, where);\n return {\n message: read.text(error['message'], `${where}.message`),\n raw: read.optionalText(error['raw'], `${where}.raw`) ?? '',\n spec: spec(error['spec'], `${where}.spec`),\n };\n });\n return { results, errors };\n}\n\nconst SEVERITIES: readonly string[] = ['error', 'warning', 'note'];\n\n/**\n * What `spec-brief archive --dry-run --format json` would refuse and warn\n * about. A document with no plan plans nothing to refuse. spec-brief gives a\n * reason without a hint where it has no next step, and the reason is carried\n * as it gave it: a field it left out is undefined, which JSON leaves out too.\n */\nexport function readArchivePlan(document: unknown): { blocking: ArchiveReason[]; warnings: ArchiveReason[] } {\n const read = reader('spec-brief archive');\n const top = read.object(document, 'a document');\n if (top['plan'] === undefined) return { blocking: [], warnings: [] };\n const plan = read.object(top['plan'], 'plan');\n const reasons = (key: 'blocking' | 'warnings'): ArchiveReason[] =>\n read.list(plan[key], `plan.${key}`).map((value, index) => {\n const where = `plan.${key}[${index}]`;\n const reason = read.object(value, where);\n const severity = read.text(reason['severity'], `${where}.severity`);\n if (!SEVERITIES.includes(severity)) read.refuse(`${where}.severity`, 'error, warning or note');\n const hint = read.optionalText(reason['hint'], `${where}.hint`);\n const file = read.optionalText(reason['file'], `${where}.file`);\n const line = read.optionalNumber(reason['line'], `${where}.line`);\n const path = read.optionalText(reason['path'], `${where}.path`);\n return {\n rule: read.text(reason['rule'], `${where}.rule`),\n severity: severity as ArchiveReason['severity'],\n message: read.text(reason['message'], `${where}.message`),\n hint,\n file,\n line,\n path,\n } as ArchiveReason;\n });\n return { blocking: reasons('blocking'), warnings: reasons('warnings') };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What spec-guard and spec-brief answer in JSON, besides the brief list\n * briefs.ts reads: a query's rules, a run's results and errors, and an\n * archive's plan. Each is checked for the fields the harness reads, with the\n * types it reads them as, and a document of another shape is an error that\n * names the tool and the field (ADR-0002): the command that asked stops with\n * exit 2, as for any sibling that printed something it cannot read, rather\n * than crash halfway through the document or count what it could not read as\n * clean. Fields the harness does not read are not checked, so a sibling may\n * add to its documents freely.\n */\n\nimport type { ArchiveReason } from './audit.js';\nimport { SiblingOutputError } from './briefs.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads fields of one document, naming the tool, its command and the field in every refusal. */\nfunction reader(printer: string) {\n const refuse = (where: string, expected: string): never => {\n throw new SiblingOutputError(`${printer} printed ${where} that is not ${expected}`);\n };\n return {\n object(value: unknown, where: string): Json {\n return isObject(value) ? value : refuse(where, 'an object');\n },\n /** A list that is absent is an empty one. */\n list(value: unknown, where: string): unknown[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? (value as unknown[]) : refuse(where, 'a list');\n },\n text(value: unknown, where: string): string {\n return typeof value === 'string' ? value : refuse(where, 'text');\n },\n optionalText(value: unknown, where: string): string | undefined {\n return value === undefined ? undefined : typeof value === 'string' ? value : refuse(where, 'text');\n },\n number(value: unknown, where: string): number {\n return typeof value === 'number' ? value : refuse(where, 'a number');\n },\n optionalNumber(value: unknown, where: string): number | undefined {\n return value === undefined ? undefined : typeof value === 'number' ? value : refuse(where, 'a number');\n },\n flag(value: unknown, where: string): boolean {\n return typeof value === 'boolean' ? value : refuse(where, 'true or false');\n },\n refuse,\n };\n}\n\n/** Where spec-guard places a directive. */\nexport interface Spec {\n readonly file: string;\n readonly line: number;\n}\n\nexport interface QueryRule {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n /** Absent from a document that says nothing of it, which is in force. */\n readonly inForce: boolean;\n}\n\n/** The rules in each of the results of `spec-guard query --json`, in order. */\nexport function readQueryRules(document: unknown): QueryRule[][] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['results'], 'results').map((value, index) => {\n const result = read.object(value, `results[${index}]`);\n return read.list(result['rules'], `results[${index}].rules`).map((ruleValue, at) => {\n const where = `results[${index}].rules[${at}]`;\n const rule = read.object(ruleValue, where);\n const reason = rule['reason'];\n if (reason !== undefined && reason !== null && typeof reason !== 'string') read.refuse(`${where}.reason`, 'text or null');\n const inForce = rule['inForce'];\n return {\n document: read.text(rule['document'], `${where}.document`),\n line: read.number(rule['line'], `${where}.line`),\n kind: read.text(rule['kind'], `${where}.kind`),\n description: read.text(rule['description'], `${where}.description`),\n reason: typeof reason === 'string' ? reason : null,\n inForce: inForce === undefined ? true : read.flag(inForce, `${where}.inForce`),\n };\n });\n });\n}\n\n/**\n * The spec patterns `spec-guard query --json` says matched no file, as\n * written. A document that says nothing of them names none: spec-guard has\n * written the field since 0.22.0, and only where there is such a pattern.\n */\nexport function readQueryUnmatched(document: unknown): string[] {\n const read = reader('spec-guard query');\n const top = read.object(document, 'a document');\n return read.list(top['unmatchedSpecs'], 'unmatchedSpecs').map((value, index) => read.text(value, `unmatchedSpecs[${index}]`));\n}\n\nexport interface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec | undefined;\n}\n\nexport interface GuardError {\n readonly message: string;\n readonly raw: string;\n readonly spec?: Spec | undefined;\n}\n\n/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */\nexport function readGuardRun(document: unknown): { results: GuardResult[]; errors: GuardError[] } {\n const read = reader('spec-guard');\n const top = read.object(document, 'a document');\n const spec = (value: unknown, where: string): Spec | undefined => {\n if (value === undefined) return undefined;\n const placed = read.object(value, where);\n return { file: read.text(placed['file'], `${where}.file`), line: read.number(placed['line'], `${where}.line`) };\n };\n const results = read.list(top['results'], 'results').map((value, index) => {\n const where = `results[${index}]`;\n const result = read.object(value, where);\n return {\n ok: read.flag(result['ok'], `${where}.ok`),\n description: read.text(result['description'], `${where}.description`),\n message: read.text(result['message'], `${where}.message`),\n spec: spec(result['spec'], `${where}.spec`),\n };\n });\n const errors = read.list(top['errors'], 'errors').map((value, index) => {\n const where = `errors[${index}]`;\n const error = read.object(value, where);\n return {\n message: read.text(error['message'], `${where}.message`),\n raw: read.optionalText(error['raw'], `${where}.raw`) ?? '',\n spec: spec(error['spec'], `${where}.spec`),\n };\n });\n return { results, errors };\n}\n\nconst SEVERITIES: readonly string[] = ['error', 'warning', 'note'];\n\n/**\n * What `spec-brief archive --dry-run --format json` would refuse and warn\n * about. A document with no plan plans nothing to refuse. spec-brief gives a\n * reason without a hint where it has no next step, and the reason is carried\n * as it gave it: a field it left out is undefined, which JSON leaves out too.\n */\nexport function readArchivePlan(document: unknown): { blocking: ArchiveReason[]; warnings: ArchiveReason[] } {\n const read = reader('spec-brief archive');\n const top = read.object(document, 'a document');\n if (top['plan'] === undefined) return { blocking: [], warnings: [] };\n const plan = read.object(top['plan'], 'plan');\n const reasons = (key: 'blocking' | 'warnings'): ArchiveReason[] =>\n read.list(plan[key], `plan.${key}`).map((value, index) => {\n const where = `plan.${key}[${index}]`;\n const reason = read.object(value, where);\n const severity = read.text(reason['severity'], `${where}.severity`);\n if (!SEVERITIES.includes(severity)) read.refuse(`${where}.severity`, 'error, warning or note');\n const hint = read.optionalText(reason['hint'], `${where}.hint`);\n const file = read.optionalText(reason['file'], `${where}.file`);\n const line = read.optionalNumber(reason['line'], `${where}.line`);\n const path = read.optionalText(reason['path'], `${where}.path`);\n return {\n rule: read.text(reason['rule'], `${where}.rule`),\n severity: severity as ArchiveReason['severity'],\n message: read.text(reason['message'], `${where}.message`),\n hint,\n file,\n line,\n path,\n } as ArchiveReason;\n });\n return { blocking: reasons('blocking'), warnings: reasons('warnings') };\n}\n"]}
|
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
|
-
/**
|
|
23
|
-
|
|
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
|
|
42
|
-
* the branch - never guessed from which brief happens to
|
|
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
|
|
88
|
-
* the branch - never guessed from which brief happens to
|
|
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() !== '')
|
package/dist/briefs.js.map
CHANGED
|
@@ -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":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAEtE,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAID,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CAAC,KAAc,EAAE,KAAa;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,2BAA2B,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CAAC,KAAc,EAAE,KAAa;IACnD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,kBAAkB,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,QAAiB;IAC9C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,YAAY,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,MAAM,EAAE,CAAC;QAC/F,MAAM,IAAI,kBAAkB,CAAC,0CAA0C,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IAC1C,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,kBAAkB,CAC1B,oCAAoC,MAAM,CAAC,OAAO,CAAC,wBAAwB,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClH,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,kBAAkB,CAAC,iCAAiC,CAAC,CAAC;IAC5F,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,mBAAmB,CAAC,CAAC;QAC9E,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,oBAAoB,CAAC,CAAC;QACnH,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,UAAU;YAAE,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,cAAc,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnH,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,OAAO;YACL,EAAE;YACF,IAAI;YACJ,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,QAAQ,CAAC;YACrD,KAAK;YACL,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,SAAS,CAAC;YACxD,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;YAClD,IAAI,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;YAC5C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,YAAY,CAAC;YAChE,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,gBAAgB,CAAC;YAC5E,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,iBAAiB,CAAC;YAC/E,KAAK,EAAE;gBACL,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjF,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;aACxF;YACD,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI;YAC5B,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,YAAY,CAAC;SACjE,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAeD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,MAA2B,EAC3B,KAKC;IAED,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,MAAoB,EAAe,EAAE;QAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACjF,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC,CAAC;IACF,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;IACjH,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IACjG,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,CAAC,CAAC;IAC7H,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC7F,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,yGAAyG;KAClH,CAAC;AACJ,CAAC","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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type CliIO } from './workspace.js';
|
|
13
13
|
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
|
|
14
|
-
export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\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,
|
|
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\nline it cannot read, and a --root that is not a directory, with exit 1: it refuses\nnothing for either.\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n";
|
|
15
15
|
/**
|
|
16
16
|
* Runs the command line and resolves to the exit code, for an error nothing
|
|
17
17
|
* below expected too.
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { mcpCommand } from './server.js';
|
|
|
24
24
|
import { gitHook, initCommand } from './setup.js';
|
|
25
25
|
import { notFido2, readAllowedSigners } from './signers.js';
|
|
26
26
|
import { SiblingError } from './siblings.js';
|
|
27
|
-
import { emptyValue, isBlank, refusal } from './usage.js';
|
|
27
|
+
import { emptyValue, isBlank, isClaudeHook, noDirectory, refusal } from './usage.js';
|
|
28
28
|
import { checkClaudeCode, CLAUDE_CODE_MINIMUM, MINIMUM_VERSIONS } from './versions.js';
|
|
29
29
|
import { activeBrief, describeActive, EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, namedId, openWorkspace, parseOptions, UsageError, version, } from './workspace.js';
|
|
30
30
|
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './workspace.js';
|
|
@@ -41,7 +41,8 @@ Commands:
|
|
|
41
41
|
hook git Answer git's pre-commit hook for the staged files.
|
|
42
42
|
escalate Ask a person to rule: --path <file> (repeatable) --reason <why>
|
|
43
43
|
[--option "<label>: <cost>"]... [--recommend <text>].
|
|
44
|
-
--list shows what waits; --show <id> prints one memo.
|
|
44
|
+
--list shows what waits; --show <id> prints one memo. Neither
|
|
45
|
+
raises a request: beside one, only --root and --format are read.
|
|
45
46
|
rule <id> Record a person's ruling in the brief: --allow or --deny, with
|
|
46
47
|
--note <text>. It counts once committed signed.
|
|
47
48
|
rulings [brief] The brief's rulings, and whether each one's signature verifies.
|
|
@@ -74,10 +75,12 @@ Options:
|
|
|
74
75
|
--strict Warnings fail the run. For guard, audit, premises and doctor.
|
|
75
76
|
--help, --version
|
|
76
77
|
|
|
77
|
-
An option a command does not read, an argument it does not take,
|
|
78
|
-
argument given an empty value
|
|
79
|
-
|
|
80
|
-
as every release has
|
|
78
|
+
An option a command does not read, an argument it does not take, an option or an
|
|
79
|
+
argument given an empty value, and a brief given both as the argument and as --brief
|
|
80
|
+
are refused, exit 2, not read as if they were not there. doctor says an empty --brief
|
|
81
|
+
or --base in its report. hook claude reads its line as every release has, and says a
|
|
82
|
+
line it cannot read, and a --root that is not a directory, with exit 1: it refuses
|
|
83
|
+
nothing for either.
|
|
81
84
|
|
|
82
85
|
Exit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.
|
|
83
86
|
`;
|
|
@@ -127,32 +130,75 @@ async function readStdin(io) {
|
|
|
127
130
|
chunks.push(chunk);
|
|
128
131
|
return Buffer.concat(chunks).toString('utf8');
|
|
129
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The write Claude Code asks the hook about, or the exit code that answers
|
|
135
|
+
* already: 1 for a question that cannot be read, 0 for a tool that writes
|
|
136
|
+
* no file. Asked before anything on the hook's own line is, so that a line
|
|
137
|
+
* that is wrong is said where there is a write it leaves unchecked.
|
|
138
|
+
*/
|
|
139
|
+
async function writeAsked(io) {
|
|
140
|
+
let question;
|
|
141
|
+
try {
|
|
142
|
+
question = await readStdin(io);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// A question that could not be read is one the hook cannot see, as one
|
|
146
|
+
// it cannot parse is, below: it has no event to hold a write by.
|
|
147
|
+
io.stderr.write(`spec-harness: the hook input could not be read: ${error.message}\n`);
|
|
148
|
+
return EXIT_FAILED;
|
|
149
|
+
}
|
|
150
|
+
const request = parseClaudeHook(question);
|
|
151
|
+
if ('error' in request) {
|
|
152
|
+
// A hook that cannot read its question refuses nothing it cannot see,
|
|
153
|
+
// but says so where the person reads it: a non-blocking error.
|
|
154
|
+
io.stderr.write(`spec-harness: ${request.error}\n`);
|
|
155
|
+
return EXIT_FAILED;
|
|
156
|
+
}
|
|
157
|
+
return request.paths.length === 0 ? EXIT_OK : request;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* What is wrong with the hook's own line, said and nothing refused for it:
|
|
161
|
+
* one line, since Claude Code shows the first line of the stderr of a hook
|
|
162
|
+
* that exits 1 and goes on with the write, where exit 2 would hold every
|
|
163
|
+
* write of the session, the one that mends the line among them (ADR-0005).
|
|
164
|
+
*/
|
|
165
|
+
function guardOff(io, wrong, mend) {
|
|
166
|
+
io.stderr.write(`spec-harness: ${wrong}, so the guard is off and this write is not checked; ${mend}\n`);
|
|
167
|
+
return EXIT_FAILED;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Claude Code's hook on a line the parser refused: an option no command has,
|
|
171
|
+
* an option left without its value, a --format or an --at that is none. It
|
|
172
|
+
* was exit 2 before the question was read, for the line a settings file
|
|
173
|
+
* holds as for one typed at a terminal. The parser's words are kept, on one
|
|
174
|
+
* line: what node says of an option that may have lost its value is three.
|
|
175
|
+
*/
|
|
176
|
+
async function unreadHookLine(said, io) {
|
|
177
|
+
const request = await writeAsked(io);
|
|
178
|
+
if (typeof request === 'number')
|
|
179
|
+
return request;
|
|
180
|
+
return guardOff(io, `the hook's line cannot be read (${said.split('\n').join(' ')})`, 'mend it in Claude Code\'s settings, where init writes "hook claude" and nothing after it');
|
|
181
|
+
}
|
|
130
182
|
async function hookCommand(options, io) {
|
|
131
183
|
const kind = options.positionals[0];
|
|
132
184
|
if (kind === 'claude') {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
const request = await writeAsked(io);
|
|
186
|
+
if (typeof request === 'number')
|
|
187
|
+
return request;
|
|
188
|
+
// An empty root is the directory the hook was started in: git starts
|
|
189
|
+
// there when it is handed no directory, so the hook has guarded there
|
|
190
|
+
// for an empty root in every release.
|
|
191
|
+
const root = options.root === '' ? io.cwd : options.root;
|
|
192
|
+
// A root written on the hook's line that is a file, or is not there: git
|
|
193
|
+
// starts in neither, which read as a session outside a work tree, exit 0
|
|
194
|
+
// and no word, and the guard someone had configured was off.
|
|
195
|
+
if (root !== undefined && !isDirectory(root)) {
|
|
196
|
+
return guardOff(io, noDirectory(root), "give the hook's line a directory that exists, or leave --root out for the project Claude Code names");
|
|
136
197
|
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
// A question that could not be read is one the hook cannot see, as one
|
|
139
|
-
// it cannot parse is, below: it has no event to hold a write by.
|
|
140
|
-
io.stderr.write(`spec-harness: the hook input could not be read: ${error.message}\n`);
|
|
141
|
-
return EXIT_FAILED;
|
|
142
|
-
}
|
|
143
|
-
const request = parseClaudeHook(question);
|
|
144
|
-
if ('error' in request) {
|
|
145
|
-
// A hook that cannot read its question refuses nothing it cannot see,
|
|
146
|
-
// but says so where the person reads it: a non-blocking error.
|
|
147
|
-
io.stderr.write(`spec-harness: ${request.error}\n`);
|
|
148
|
-
return EXIT_FAILED;
|
|
149
|
-
}
|
|
150
|
-
if (request.paths.length === 0)
|
|
151
|
-
return EXIT_OK;
|
|
152
198
|
const cwd = request.cwd ?? io.cwd;
|
|
153
199
|
let workspace;
|
|
154
200
|
try {
|
|
155
|
-
workspace = await openWorkspace({ ...options, root:
|
|
201
|
+
workspace = await openWorkspace({ ...options, root: root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);
|
|
156
202
|
}
|
|
157
203
|
catch (error) {
|
|
158
204
|
if (error instanceof UsageError)
|
|
@@ -356,7 +402,13 @@ async function dispatch(argv, io) {
|
|
|
356
402
|
options = parseOptions(argv);
|
|
357
403
|
}
|
|
358
404
|
catch (error) {
|
|
359
|
-
|
|
405
|
+
// The parser throws nothing but its refusal of the line, which holds
|
|
406
|
+
// whose line it was. Claude Code's hook says its own, and holds no
|
|
407
|
+
// write for it.
|
|
408
|
+
const refused = error;
|
|
409
|
+
if (isClaudeHook(refused.words[0], refused.words[1]))
|
|
410
|
+
return unreadHookLine(refused.message, io);
|
|
411
|
+
io.stderr.write(`spec-harness: ${refused.message}\n`);
|
|
360
412
|
return EXIT_ERROR;
|
|
361
413
|
}
|
|
362
414
|
if (options.version) {
|