@descent-vtt/spec-harness 0.1.0 → 0.1.2
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/.mcp.json +5 -4
- package/CHANGELOG.md +69 -1
- package/README.md +22 -12
- package/dist/audit.d.ts +23 -2
- package/dist/audit.js +30 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +30 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/configure.d.ts +105 -0
- package/dist/configure.js +210 -0
- package/dist/configure.js.map +1 -0
- package/dist/context.d.ts +31 -4
- package/dist/context.js +40 -7
- package/dist/context.js.map +1 -1
- package/dist/fs.d.ts +2 -0
- package/dist/fs.js +12 -0
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +2 -0
- package/dist/git.js +5 -0
- package/dist/git.js.map +1 -1
- package/dist/guard.d.ts +2 -0
- package/dist/guard.js +5 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +4 -9
- package/dist/plugin.js.map +1 -1
- package/dist/premises.d.ts +4 -0
- package/dist/premises.js +20 -14
- package/dist/premises.js.map +1 -1
- package/dist/round.d.ts +7 -3
- package/dist/round.js +35 -26
- package/dist/round.js.map +1 -1
- package/dist/server.js +37 -13
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +0 -9
- package/dist/setup.js +85 -68
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +2 -1
- package/dist/siblings.js +1 -1
- package/dist/siblings.js.map +1 -1
- package/dist/versions.js +2 -2
- package/dist/versions.js.map +1 -1
- package/hooks/hooks.json +12 -2
- package/package.json +4 -5
- package/skills/close-round/SKILL.md +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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 @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/.mcp.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"spec-harness": {
|
|
4
|
-
"command": "
|
|
4
|
+
"command": "node",
|
|
5
5
|
"args": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js",
|
|
7
|
+
"mcp",
|
|
8
|
+
"--root",
|
|
9
|
+
"${CLAUDE_PROJECT_DIR}"
|
|
9
10
|
]
|
|
10
11
|
}
|
|
11
12
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
A signed ruling now works from `init` onwards. An end-to-end check of 0.1.1
|
|
6
|
+
from npm, in a repository made with `git init`, could not finish the round
|
|
7
|
+
the README describes: `init` named no base and did not load the spec-brief
|
|
8
|
+
plugin, so no ruling verified, and spec-brief's archive refused the file a
|
|
9
|
+
person had allowed. After upgrading, run `npx spec-harness init --write`
|
|
10
|
+
again: it adds what is missing and replaces the `npx` hooks and server 0.1
|
|
11
|
+
wrote.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `init` loads this package's plugin in spec-brief's configuration,
|
|
16
|
+
`"plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"]`, in whichever
|
|
17
|
+
file spec-brief reads. Without it, spec-brief's archive, and so `audit`,
|
|
18
|
+
refused a protected file whatever ruling was signed. `doctor` says whether
|
|
19
|
+
spec-brief loads the plugin, and `audit` gives it as the reason for such a
|
|
20
|
+
refusal, where it passed on spec-brief's "record the departure in the
|
|
21
|
+
brief".
|
|
22
|
+
- `init` names the base in `.spec-harness.json`: the remote's default branch
|
|
23
|
+
where git recorded one, otherwise `main`, `master` or the only branch when
|
|
24
|
+
`init` runs on it; otherwise it asks. git records a remote's default
|
|
25
|
+
branch when it clones, and before 2.48 not on a fetch, so a repository
|
|
26
|
+
made with `git init` and pushed to a remote often has none. 0.1 wrote `{}`
|
|
27
|
+
there, and no command could verify a ruling. It adds the base to a configuration that names
|
|
28
|
+
none. `doctor` shows the base, where it came from, and whether the
|
|
29
|
+
allowed-signers file is on it.
|
|
30
|
+
- `guard`, `context` and the hooks honour `--base`, which they ignored. The
|
|
31
|
+
MCP tools `check_path` and `start_round` take a `base` argument, as
|
|
32
|
+
`audit_round` does.
|
|
33
|
+
- The plugin's MCP server and hooks, and those `init` writes, run `node`
|
|
34
|
+
with `${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`.
|
|
35
|
+
Claude Code starts a plugin's server in the plugin's directory, where
|
|
36
|
+
`npx --no-install spec-harness` found no spec-harness, and on Windows
|
|
37
|
+
starts a server without a shell, where `npx` cannot start at all. The
|
|
38
|
+
server is told the project with `--root`, and reads `CLAUDE_PROJECT_DIR`
|
|
39
|
+
when no root is named. Use the plugin or `init`'s hooks and server, not
|
|
40
|
+
both.
|
|
41
|
+
- `context` says spec-guard holds no rule when no spec file matches its
|
|
42
|
+
patterns, rather than that it could not read its specs, and passes on what
|
|
43
|
+
spec-guard said when it could not.
|
|
44
|
+
- `premises` reports the premise of the brief a round works on as
|
|
45
|
+
`premise-retired`, a note, as `audit` does, and no longer fails on the
|
|
46
|
+
round's own branch.
|
|
47
|
+
- `init` says whether spec-graph's patterns reach the briefs, which its
|
|
48
|
+
defaults do not in `briefs/`, and merges into the configuration spec-graph
|
|
49
|
+
reads rather than shadowing `spec-graph.config.json` or a `"spec-graph"`
|
|
50
|
+
key in `package.json`.
|
|
51
|
+
- A title written with an em dash after the id, as `spec-brief new` writes
|
|
52
|
+
it, no longer repeats the id in `list_rounds` and `context`.
|
|
53
|
+
- The README's links to the ADRs work in `node_modules` and on npmjs.com.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- The programmatic API exports `GUARD_HOOK`, the guard as a Claude Code
|
|
58
|
+
hook, in place of `HOOK_COMMAND`, and `mcpServer`, `PROJECT_DIR` and
|
|
59
|
+
`PROJECT_DIR_OR_HERE` for the server.
|
|
60
|
+
|
|
61
|
+
## 0.1.1
|
|
62
|
+
|
|
63
|
+
The same code as 0.1.0, built by CI from its tag and published with a
|
|
64
|
+
provenance attestation. 0.1.0 was published from a workstation by mistake, an
|
|
65
|
+
`npm publish` run in a checkout, so it has none; it is deprecated in favour
|
|
66
|
+
of this one, and nothing else changed between them.
|
|
67
|
+
|
|
68
|
+
- `npm publish` in a checkout now refuses to run outside GitHub Actions.
|
|
69
|
+
CI never runs it: it stages a tarball it packed, with scripts off.
|
|
70
|
+
|
|
3
71
|
## 0.1.0
|
|
4
72
|
|
|
5
73
|
The first release of spec-harness, the agent-facing member of the spec-*
|
|
@@ -13,7 +81,7 @@ are installed; an older one is reported with its minimum and never run.
|
|
|
13
81
|
This version was staged on npm by CI from its tag, with provenance, and
|
|
14
82
|
released by a maintainer with a second factor. 0.0.0 on npm is a placeholder
|
|
15
83
|
without code, published by hand to claim the name, and is deprecated
|
|
16
|
-
([ADR-0011](docs/adr/0011-releases-are-staged-by-ci.md)).
|
|
84
|
+
([ADR-0011](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0011-releases-are-staged-by-ci.md)).
|
|
17
85
|
|
|
18
86
|
### Added
|
|
19
87
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npx spec-harness init # the plan: what it would configure, and why
|
|
|
10
10
|
npx spec-harness init --write # apply it
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
spec-brief 0.2.0 or later is required: the brief is the contract, spec-brief is its reader, and 0.2.0 is the first whose archive asks this package's plugin about signed rulings. spec-guard 0.12.0 and spec-graph 0.9.0 or later are used when they are installed, and their absence is reported, never assumed clean. A sibling installed below its minimum is never run: `doctor` and every command that needed it name the minimum ([ADR-0011](docs/adr/0011-releases-are-staged-by-ci.md)).
|
|
13
|
+
spec-brief 0.2.0 or later is required: the brief is the contract, spec-brief is its reader, and 0.2.0 is the first whose archive asks this package's plugin about signed rulings. spec-guard 0.12.0 and spec-graph 0.9.0 or later are used when they are installed, and their absence is reported, never assumed clean. A sibling installed below its minimum is never run: `doctor` and every command that needed it name the minimum ([ADR-0011](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0011-releases-are-staged-by-ci.md)).
|
|
14
14
|
|
|
15
15
|
## A round
|
|
16
16
|
|
|
@@ -30,15 +30,15 @@ person approves brief 012 ─▶ agent on branch brief/012-rotate-tokens
|
|
|
30
30
|
|
|
31
31
|
### `context [brief]`
|
|
32
32
|
|
|
33
|
-
The brief in full - it is the contract - then what it implies: the files the round may write and must not change as the guard reads them, the rulings already signed, the briefs it depends on and whether they are done, the architecture rules spec-guard holds that code to, and the documents the brief links to, whole, until the packet reaches its budget (`context.budget`, 60,000 characters). Documents left out are named by path, not dropped.
|
|
33
|
+
The brief in full - it is the contract - then what it implies: the files the round may write and must not change as the guard reads them, the rulings already signed, the briefs it depends on and whether they are done, the architecture rules spec-guard holds that code to, and the documents the brief links to, whole, until the packet reaches its budget (`context.budget`, 60,000 characters). Documents left out are named by path, not dropped. 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. `--base` names the base the rulings are verified against.
|
|
34
34
|
|
|
35
35
|
### `guard <path...>` and the hooks
|
|
36
36
|
|
|
37
37
|
May the round write this? A path the brief protects is **refused** unless a signed ruling allows it; a path in `affectedFiles` is allowed; anything else is **outside the scope** - a warning by default, or a question to the person, or a refusal (`outOfScope`: `warn`, `ask`, `deny`). Paths are compared as the filesystem spells them, links resolved and case corrected, so `SRC/db/schema.ts` does not walk past a protection on `src/db/schema.ts` on Windows.
|
|
38
38
|
|
|
39
|
-
`spec-harness hook claude` answers Claude Code's PreToolUse and PostToolUse hooks: a refusal before the write, with the reason and the next step; a warning after a write outside the scope. It never answers `allow`, which would skip the person's own permission prompt. `spec-harness hook git` is a pre-commit hook for any agent or none. A guard is a guardrail - an agent that writes through a shell passes it - so the audit and spec-brief's archive are the gates ([ADR-0005](docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
39
|
+
`spec-harness hook claude` answers Claude Code's PreToolUse and PostToolUse hooks: a refusal before the write, with the reason and the next step; a warning after a write outside the scope. It never answers `allow`, which would skip the person's own permission prompt. `spec-harness hook git` is a pre-commit hook for any agent or none. A guard is a guardrail - an agent that writes through a shell passes it - so the audit and spec-brief's archive are the gates ([ADR-0005](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0005-a-guard-is-a-guardrail.md)).
|
|
40
40
|
|
|
41
|
-
**Which brief?** `--brief <id>`, then `SPEC_BRIEF`, then the branch name (`brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}`). Never guessed ([ADR-0004](docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
|
|
41
|
+
**Which brief?** `--brief <id>`, then `SPEC_BRIEF`, then the branch name (`brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}`). Never guessed ([ADR-0004](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0004-the-active-brief-is-named-not-guessed.md)).
|
|
42
42
|
|
|
43
43
|
### `escalate`, `rule`, `rulings`
|
|
44
44
|
|
|
@@ -55,7 +55,9 @@ npx spec-harness rule E-012-1 --allow --note "Add rotated_at only."
|
|
|
55
55
|
git commit -S -m "ruling R-012-1: allow" -- briefs/012_rotate-tokens.md
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
A ruling is a row in the brief's `## Rulings` table. It **counts** when the commit that last changed the row is signed by a key the **base branch's** `.github/allowed_signers` lists. An agent can write a row and compute any hash; it cannot produce the person's signature, and an edit to the row moves it to a commit that must be signed again ([ADR-0006](docs/adr/0006-a-ruling-is-a-signed-commit.md)). Use a FIDO2 key (`ed25519-sk`) where the agent runs as you: its signature needs a touch no process can supply.
|
|
58
|
+
A ruling is a row in the brief's `## Rulings` table. It **counts** when the commit that last changed the row is signed by a key the **base branch's** `.github/allowed_signers` lists. An agent can write a row and compute any hash; it cannot produce the person's signature, and an edit to the row moves it to a commit that must be signed again ([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)). Use a FIDO2 key (`ed25519-sk`) where the agent runs as you: its signature needs a touch no process can supply.
|
|
59
|
+
|
|
60
|
+
spec-brief's archive refuses a round that changed a protected file, and learns that a signed ruling allows it only from this package's plugin: `init` adds `"plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"]` to spec-brief's configuration. Without it, the archive refuses the file whatever was signed; `doctor` says whether spec-brief loads the plugin, and `audit` names it as the reason for such a refusal.
|
|
59
61
|
|
|
60
62
|
### `audit [brief]`
|
|
61
63
|
|
|
@@ -78,11 +80,11 @@ signature: expected 401, got 200
|
|
|
78
80
|
```
|
|
79
81
|
````
|
|
80
82
|
|
|
81
|
-
`probe --at base` runs it in a temporary worktree at the base commit: every run must fail **for the declared reason** - the `signature` in the output, or a JUnit `test` failing - or the verdict is `vacuous` (no defect), `flaky` or `invalid`. `probe --at head` must be green: `fixed`. The evidence table it prints names the hash of the probe it measured with ([ADR-0007](docs/adr/0007-probes-declare-their-failure.md)).
|
|
83
|
+
`probe --at base` runs it in a temporary worktree at the base commit: every run must fail **for the declared reason** - the `signature` in the output, or a JUnit `test` failing - or the verdict is `vacuous` (no defect), `flaky` or `invalid`. `probe --at head` must be green: `fixed`. The evidence table it prints names the hash of the probe it measured with ([ADR-0007](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0007-probes-declare-their-failure.md)).
|
|
82
84
|
|
|
83
85
|
### `premises`
|
|
84
86
|
|
|
85
|
-
Is every live brief still about something true? It runs spec-guard over the live briefs' assertions and reports each premise - an assertion under a section in `assertions.premises` - that no longer holds as `stale-premise`: the defect was fixed another way, or the code the brief describes is gone, and an agent sent to fix it would fix nothing. Goals are left to `audit`, since a goal fails until its round is done. Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not there to ask.
|
|
87
|
+
Is every live brief still about something true? It runs spec-guard over the live briefs' assertions and reports each premise - an assertion under a section in `assertions.premises` - that no longer holds as `stale-premise`: the defect was fixed another way, or the code the brief describes is gone, and an agent sent to fix it would fix nothing. Goals are left to `audit`, since a goal fails until its round is done. For the same reason, the premise of the brief a round is working on - the one `--brief`, `SPEC_BRIEF` or the branch names - is reported as `audit` reports it, `premise-retired`, a note that fails nothing: on the round's branch, a premise that no longer holds is the work being done. Run it in CI: exit 1 when a premise is stale, exit 2 when spec-guard is not there to ask.
|
|
86
88
|
|
|
87
89
|
### `mcp`
|
|
88
90
|
|
|
@@ -90,21 +92,29 @@ Is every live brief still about something true? It runs spec-guard over the live
|
|
|
90
92
|
|
|
91
93
|
### `init`
|
|
92
94
|
|
|
93
|
-
Configures the family to agree
|
|
95
|
+
Configures the family to agree. It prints the plan, merges into files that exist, keeping what is there, and changes nothing without `--write`:
|
|
96
|
+
|
|
97
|
+
- **spec-brief**: `spec-brief init` when there is no configuration, then this package's plugin in its `plugins`, `"@descent-vtt/spec-harness/spec-brief-plugin"`. spec-brief's archive asks the plugin whether a signed ruling allows a protected file, and refuses the file without it.
|
|
98
|
+
- **spec-graph**, when it is installed: the archive in its `historyPatterns`, so an archived brief is a record whatever its status says, and a live brief that depends on one is not a stale premise. That holds only where spec-graph reads the briefs, and its default patterns (`docs/`, `doc/`, `adr/`, `rfcs/`, `specs/` and Markdown at the root) do not reach `briefs/`: `init` says whether they do, and leaves adding the briefs to `patterns` to you, since that changes what spec-graph checks, and its `patterns` replace its defaults. It merges into the configuration spec-graph reads, and leaves a `"spec-graph"` key in `package.json` to you.
|
|
99
|
+
- **`.spec-harness.json`**, naming the base rounds are measured from and the allowed signers are read on: the remote's default branch where git recorded one, as a clone does and git 2.48 or later does on a fetch; otherwise the branch `init` runs on when it is `main` or `master`, or the only branch, since a repository made with `git init` and pushed to a remote often records no default. When it cannot tell, it says so and writes none: set `"base"` yourself.
|
|
100
|
+
- **Claude Code**: the guard hooks in `.claude/settings.json` and the MCP server in `.mcp.json`, each run with `node` from the project's `node_modules`. These are the ones the [plugin](#as-a-claude-code-plugin) brings: use one or the other. The `npx` entries 0.1 wrote are replaced.
|
|
101
|
+
- With `--git-hook`, a pre-commit hook.
|
|
94
102
|
|
|
95
103
|
### `doctor`
|
|
96
104
|
|
|
97
|
-
Which sibling tools are installed, at which versions, and how each is run, the repository root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names. A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1. A command named under `tools` is run as named, and its version is not checked. The first thing to run when a hook refuses something unexpectedly.
|
|
105
|
+
Which sibling tools are installed, at which versions, and how each is run, the repository root, the branch, and the brief the flag, `SPEC_BRIEF` or the branch names. Then what a signed ruling needs to count: the base, and whether `--base`, `.spec-harness.json` or the remote named it; whether the allowed-signers file is on that base; and whether spec-brief loads this package's plugin. A sibling older than this release needs is `outdated`, with the minimum and the command that installs a newer one, and exit 1. A command named under `tools` is run as named, and its version is not checked. The first thing to run when a hook refuses something unexpectedly.
|
|
98
106
|
|
|
99
107
|
## As a Claude Code plugin
|
|
100
108
|
|
|
101
|
-
The repository is a plugin and a one-plugin marketplace: the four skills (`draft-brief`, `split-goal`, `run-round`, `close-round`), the hooks and the MCP server.
|
|
109
|
+
The repository is a plugin and a one-plugin marketplace: the four skills (`draft-brief`, `split-goal`, `run-round`, `close-round`), the hooks and the MCP server. The hooks and the server run the `spec-harness` installed in your project, `node ${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js`, so install it there first; the server is told the project with `--root`, since Claude Code starts a plugin's server in the plugin's own directory.
|
|
102
110
|
|
|
103
111
|
```text
|
|
104
112
|
/plugin marketplace add DescentVTT/spec-harness
|
|
105
113
|
/plugin install spec-harness@spec-tools
|
|
106
114
|
```
|
|
107
115
|
|
|
116
|
+
The plugin's hooks and server are the ones `init` writes into `.claude/settings.json` and `.mcp.json`: use one or the other. With both, every write is guarded twice and the server is registered twice, so with the plugin, leave those two files out of what `init` writes, or take its `spec-harness` entries out of them.
|
|
117
|
+
|
|
108
118
|
## Configuration
|
|
109
119
|
|
|
110
120
|
`.spec-harness.json` at the repository root. Every key is optional; an unknown key stops the run with exit 2.
|
|
@@ -113,7 +123,7 @@ The repository is a plugin and a one-plugin marketplace: the four skills (`draft
|
|
|
113
123
|
| --- | --- | --- |
|
|
114
124
|
| `branches` | `brief/{id}`, `brief-{id}`, `*/brief/{id}`, `*/brief-{id}` | Branch names that carry the active brief's id. |
|
|
115
125
|
| `outOfScope` | `"warn"` | A write outside `affectedFiles`: `warn`, `ask` or `deny`. |
|
|
116
|
-
| `base` | the remote's default branch | What rounds are measured from, and where allowed signers are read. |
|
|
126
|
+
| `base` | the remote's default branch | What rounds are measured from, and where allowed signers are read. `init` names it, since a repository git did not clone may have no default branch recorded. |
|
|
117
127
|
| `rulings.section` | `"Rulings"` | The brief section holding the rulings table. |
|
|
118
128
|
| `rulings.allowedSigners` | `".github/allowed_signers"` | The allowed-signers file, read from the base branch. |
|
|
119
129
|
| `dependencies.manifests` | the list above | Manifest names whose added dependencies the audit reports. |
|
|
@@ -126,7 +136,7 @@ The repository is a plugin and a one-plugin marketplace: the four skills (`draft
|
|
|
126
136
|
|
|
127
137
|
## Design
|
|
128
138
|
|
|
129
|
-
The decisions and what they cost are in [`docs/adr/`](docs/adr/README.md); what the whole family shares is [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md).
|
|
139
|
+
The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/README.md); what the whole family shares is [spec-core's ADR-0005](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md).
|
|
130
140
|
|
|
131
141
|
## License
|
|
132
142
|
|
package/dist/audit.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* A part that could not be measured is a finding, never a silence.
|
|
21
21
|
*/
|
|
22
|
+
import { type VerifiedRuling } from './guard.js';
|
|
22
23
|
import type { DependencyChange } from './manifests.js';
|
|
23
24
|
import type { BriefRow, Finding, Severity } from './types.js';
|
|
24
25
|
export interface AssertionOutcome {
|
|
@@ -31,6 +32,10 @@ export interface AssertionOutcome {
|
|
|
31
32
|
/** Every section it sits in, outermost first; `[section]` when not given. */
|
|
32
33
|
readonly enclosing?: readonly string[] | undefined;
|
|
33
34
|
}
|
|
35
|
+
/** A reason spec-brief's archive gives; a `protected-file` refusal names its `path`. */
|
|
36
|
+
export interface ArchiveReason extends Finding {
|
|
37
|
+
readonly path?: string | undefined;
|
|
38
|
+
}
|
|
34
39
|
export interface AuditInput {
|
|
35
40
|
readonly brief: BriefRow;
|
|
36
41
|
/** Why the round's changes could not be measured, when they could not. */
|
|
@@ -40,8 +45,8 @@ export interface AuditInput {
|
|
|
40
45
|
readonly unread: readonly string[];
|
|
41
46
|
};
|
|
42
47
|
readonly archive: {
|
|
43
|
-
readonly blocking: readonly
|
|
44
|
-
readonly warnings: readonly
|
|
48
|
+
readonly blocking: readonly ArchiveReason[];
|
|
49
|
+
readonly warnings: readonly ArchiveReason[];
|
|
45
50
|
} | {
|
|
46
51
|
readonly unavailable: string;
|
|
47
52
|
};
|
|
@@ -54,6 +59,10 @@ export interface AuditInput {
|
|
|
54
59
|
readonly id: string;
|
|
55
60
|
readonly reason: string;
|
|
56
61
|
}[];
|
|
62
|
+
/** The rulings whose signatures verify, which a protected file the archive refuses may be covered by. */
|
|
63
|
+
readonly verifiedRulings: readonly VerifiedRuling[];
|
|
64
|
+
/** Whether spec-brief's configuration loads this package's plugin, through which its archive learns of them. */
|
|
65
|
+
readonly pluginLoaded: boolean;
|
|
57
66
|
}
|
|
58
67
|
export interface AuditReport {
|
|
59
68
|
readonly findings: readonly Finding[];
|
|
@@ -66,4 +75,16 @@ export interface AuditReport {
|
|
|
66
75
|
* an assertion under it read as a goal would fail the round for retiring it.
|
|
67
76
|
*/
|
|
68
77
|
export declare function isPremise(sections: string | null | readonly string[], premises: readonly string[]): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* A live brief's premise that no longer holds, as `premises` reports it. On
|
|
80
|
+
* the brief a round is working on - the one the flag, SPEC_BRIEF or the
|
|
81
|
+
* branch names - that is what the round set out to do, and it is reported as
|
|
82
|
+
* the audit reports it; on any other, what the brief was written against has
|
|
83
|
+
* changed under it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function premiseFinding(brief: Pick<BriefRow, 'id' | 'file'>, outcome: {
|
|
86
|
+
readonly description: string;
|
|
87
|
+
readonly message: string;
|
|
88
|
+
readonly line: number;
|
|
89
|
+
}, active: boolean): Finding;
|
|
69
90
|
export declare function audit(input: AuditInput): AuditReport;
|
package/dist/audit.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*
|
|
20
20
|
* A part that could not be measured is a finding, never a silence.
|
|
21
21
|
*/
|
|
22
|
+
import { PLUGIN } from './configure.js';
|
|
23
|
+
import { rulingFor } from './guard.js';
|
|
22
24
|
import { sameSection } from './reader.js';
|
|
23
25
|
function finding(rule, severity, message, hint, file, line) {
|
|
24
26
|
return { rule, severity, message, hint, ...(file === undefined ? {} : { file }), ...(line === undefined ? {} : { line }) };
|
|
@@ -35,6 +37,33 @@ export function isPremise(sections, premises) {
|
|
|
35
37
|
// `## The Defect, Measured` states what was true before the round.
|
|
36
38
|
return list.some((section) => premises.some((premise) => sameSection(section, premise)));
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A live brief's premise that no longer holds, as `premises` reports it. On
|
|
42
|
+
* the brief a round is working on - the one the flag, SPEC_BRIEF or the
|
|
43
|
+
* branch names - that is what the round set out to do, and it is reported as
|
|
44
|
+
* the audit reports it; on any other, what the brief was written against has
|
|
45
|
+
* changed under it.
|
|
46
|
+
*/
|
|
47
|
+
export function premiseFinding(brief, outcome, active) {
|
|
48
|
+
if (active) {
|
|
49
|
+
return finding('premise-retired', 'note', `brief ${brief.id}'s premise no longer holds, as the round on it intends: ${outcome.description}`, 'nothing to do: this is the round that changes it, and audit measures it', brief.file, outcome.line);
|
|
50
|
+
}
|
|
51
|
+
return finding('stale-premise', 'error', `brief ${brief.id}'s premise no longer holds: ${outcome.description}: ${outcome.message}`, 'what the brief was written against has changed; archive the brief if its work is done, or rewrite its premise before a round is run on it', brief.file, outcome.line);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The next step for a reason the archive gives. A protected file a verified
|
|
55
|
+
* ruling covers is refused only because the archive did not learn of the
|
|
56
|
+
* ruling, and recording a departure in the brief would not fix that.
|
|
57
|
+
*/
|
|
58
|
+
function archiveHint(reason, input) {
|
|
59
|
+
const ruling = reason.rule === 'protected-file' && reason.path !== undefined ? rulingFor(input.verifiedRulings, reason.path) : undefined;
|
|
60
|
+
if (ruling === undefined)
|
|
61
|
+
return reason.hint;
|
|
62
|
+
const allowed = `ruling ${ruling.id}, signed by ${ruling.signer}, allows it`;
|
|
63
|
+
return input.pluginLoaded
|
|
64
|
+
? `${allowed}, and the archive still refused it: check that spec-brief loads the spec-harness installed here and measures from the same base (spec-harness doctor)`
|
|
65
|
+
: `${allowed}, but spec-brief does not load spec-harness's plugin, which is how its archive learns of signed rulings: add "${PLUGIN}" to "plugins" in its configuration, or run spec-harness init --write`;
|
|
66
|
+
}
|
|
38
67
|
export function audit(input) {
|
|
39
68
|
const { brief } = input;
|
|
40
69
|
const out = [];
|
|
@@ -46,7 +75,7 @@ export function audit(input) {
|
|
|
46
75
|
}
|
|
47
76
|
else {
|
|
48
77
|
for (const reason of [...input.archive.blocking, ...input.archive.warnings]) {
|
|
49
|
-
out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, reason
|
|
78
|
+
out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, archiveHint(reason, input), reason.file, reason.line));
|
|
50
79
|
}
|
|
51
80
|
}
|
|
52
81
|
if (!Array.isArray(input.assertions)) {
|
package/dist/audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Did the round stay inside the lines?\n *\n * One report, from three sources and one question of its own:\n *\n * - spec-brief's archive, asked with `--dry-run`: open boxes, protected files\n * changed, files outside the scope, a tree that was not committed. The\n * archive is spec-brief's decision, so its reasons are carried as they are\n * - one defect, one finding - and the harness does not measure the scope a\n * second time.\n * - spec-guard, over the brief's own assertions: the goals must hold now. A\n * premise - an assertion in a section that states what was true before the\n * round, such as \"The Defect, Measured\" - is expected to stop holding, and\n * one that still holds says the defect may still be there.\n * - The rulings: each one whose signature does not verify is reported, since\n * an unverified ruling allows nothing.\n * - And the one question nobody else asks: which dependencies the round\n * added. A dependency is code no reviewer read.\n *\n * A part that could not be measured is a finding, never a silence.\n */\n\nimport type { DependencyChange } from './manifests.js';\nimport { sameSection } from './reader.js';\nimport type { BriefRow, Finding, Severity } from './types.js';\n\nexport interface AssertionOutcome {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly line: number;\n /** The brief section the directive sits in, or `null`. */\n readonly section: string | null;\n /** Every section it sits in, outermost first; `[section]` when not given. */\n readonly enclosing?: readonly string[] | undefined;\n}\n\nexport interface AuditInput {\n readonly brief: BriefRow;\n /** Why the round's changes could not be measured, when they could not. */\n readonly unmeasured: string | null;\n readonly dependencies: { readonly changes: readonly DependencyChange[]; readonly unread: readonly string[] };\n readonly archive: { readonly blocking: readonly Finding[]; readonly warnings: readonly Finding[] } | { readonly unavailable: string };\n readonly assertions: readonly AssertionOutcome[] | { readonly unavailable: string };\n /** Section names whose assertions are premises. Compared without case, emphasis, a leading number or a trailing colon. */\n readonly premiseSections: readonly string[];\n readonly unverifiedRulings: readonly { readonly id: string; readonly reason: string }[];\n}\n\nexport interface AuditReport {\n readonly findings: readonly Finding[];\n readonly counts: Readonly<Record<Severity, number>>;\n}\n\nfunction finding(rule: string, severity: Severity, message: string, hint: string, file?: string, line?: number): Finding {\n return { rule, severity, message, hint, ...(file === undefined ? {} : { file }), ...(line === undefined ? {} : { line }) };\n}\n\n/**\n * Whether an assertion's section is a premise section. Compared as the\n * rulings section is, and as spec-brief compares section names: a heading\n * written `## 3. **The Defect, Measured:**` is still the premise section, and\n * an assertion under it read as a goal would fail the round for retiring it.\n */\nexport function isPremise(sections: string | null | readonly string[], premises: readonly string[]): boolean {\n const list = sections === null ? [] : typeof sections === 'string' ? [sections] : sections;\n // A premise section's subsections are still premises: `### Before` under\n // `## The Defect, Measured` states what was true before the round.\n return list.some((section) => premises.some((premise) => sameSection(section, premise)));\n}\n\nexport function audit(input: AuditInput): AuditReport {\n const { brief } = input;\n const out: Finding[] = [];\n\n if (input.unmeasured !== null) {\n out.push(\n finding(\n 'unmeasured',\n 'warning',\n `the round's changes were not measured: ${input.unmeasured}`,\n 'pass --base <ref>, set \"base\" in .spec-harness.json, or audit on the round\\'s branch before it merges',\n brief.file,\n ),\n );\n }\n\n if ('unavailable' in input.archive) {\n out.push(finding('archive-unchecked', 'warning', `what the archive would say is unknown: ${input.archive.unavailable}`, 'install spec-brief, or fix what stopped it', brief.file));\n } else {\n for (const reason of [...input.archive.blocking, ...input.archive.warnings]) {\n out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, reason.hint, reason.file, reason.line));\n }\n }\n\n if (!Array.isArray(input.assertions)) {\n const unavailable = (input.assertions as { unavailable: string }).unavailable;\n out.push(finding('assertions-unchecked', 'warning', `the brief's assertions were not run: ${unavailable}`, 'install spec-guard, or fix what stopped it', brief.file));\n } else {\n for (const outcome of input.assertions) {\n const premise = isPremise(outcome.enclosing ?? outcome.section, input.premiseSections);\n if (premise && outcome.ok) {\n out.push(\n finding(\n 'premise-holds',\n 'warning',\n `a premise still holds after the round: ${outcome.description}`,\n 'the round set out to change what this premise states; check that it did, or move the assertion out of the premises',\n brief.file,\n outcome.line,\n ),\n );\n } else if (premise) {\n out.push(finding('premise-retired', 'note', `a premise no longer holds, as the round intended: ${outcome.description}`, 'nothing to do', brief.file, outcome.line));\n } else if (!outcome.ok) {\n out.push(finding('goal-failed', 'error', `${outcome.description}: ${outcome.message}`, 'the round is not done until this holds', brief.file, outcome.line));\n }\n }\n }\n\n for (const ruling of input.unverifiedRulings) {\n out.push(\n finding(\n 'ruling-unverified',\n 'warning',\n `ruling ${ruling.id} allows nothing: ${ruling.reason}`,\n 'a ruling counts when the commit that last changed its row is signed by a key in the base branch\\'s allowed signers',\n brief.file,\n ),\n );\n }\n\n for (const file of input.dependencies.unread) {\n out.push(finding('manifest-unread', 'warning', `${file} changed and could not be read for dependencies`, 'check its dependencies by hand; the audit cannot', file));\n }\n for (const change of input.dependencies.changes) {\n const where = `${change.file} (${change.section})`;\n if (change.before === null) {\n out.push(\n finding(\n 'new-dependency',\n 'warning',\n `the round added ${change.ecosystem} dependency \"${change.name}\"${change.after === '' ? '' : ` ${change.after}`} in ${where}`,\n 'say in the brief why it is needed, or remove it; a new dependency is code no reviewer read',\n change.file,\n ),\n );\n } else if (change.after === null) {\n out.push(finding('dependency-removed', 'note', `the round removed \"${change.name}\" from ${where}`, 'nothing to do', change.file));\n } else {\n out.push(finding('dependency-changed', 'note', `the round moved \"${change.name}\" from ${change.before} to ${change.after} in ${where}`, 'nothing to do, if the brief meant it', change.file));\n }\n }\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, note: 0 };\n for (const item of out) counts[item.severity] += 1;\n return { findings: out, counts };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Did the round stay inside the lines?\n *\n * One report, from three sources and one question of its own:\n *\n * - spec-brief's archive, asked with `--dry-run`: open boxes, protected files\n * changed, files outside the scope, a tree that was not committed. The\n * archive is spec-brief's decision, so its reasons are carried as they are\n * - one defect, one finding - and the harness does not measure the scope a\n * second time.\n * - spec-guard, over the brief's own assertions: the goals must hold now. A\n * premise - an assertion in a section that states what was true before the\n * round, such as \"The Defect, Measured\" - is expected to stop holding, and\n * one that still holds says the defect may still be there.\n * - The rulings: each one whose signature does not verify is reported, since\n * an unverified ruling allows nothing.\n * - And the one question nobody else asks: which dependencies the round\n * added. A dependency is code no reviewer read.\n *\n * A part that could not be measured is a finding, never a silence.\n */\n\nimport { PLUGIN } from './configure.js';\nimport { rulingFor, type VerifiedRuling } from './guard.js';\nimport type { DependencyChange } from './manifests.js';\nimport { sameSection } from './reader.js';\nimport type { BriefRow, Finding, Severity } from './types.js';\n\nexport interface AssertionOutcome {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly line: number;\n /** The brief section the directive sits in, or `null`. */\n readonly section: string | null;\n /** Every section it sits in, outermost first; `[section]` when not given. */\n readonly enclosing?: readonly string[] | undefined;\n}\n\n/** A reason spec-brief's archive gives; a `protected-file` refusal names its `path`. */\nexport interface ArchiveReason extends Finding {\n readonly path?: string | undefined;\n}\n\nexport interface AuditInput {\n readonly brief: BriefRow;\n /** Why the round's changes could not be measured, when they could not. */\n readonly unmeasured: string | null;\n readonly dependencies: { readonly changes: readonly DependencyChange[]; readonly unread: readonly string[] };\n readonly archive: { readonly blocking: readonly ArchiveReason[]; readonly warnings: readonly ArchiveReason[] } | { readonly unavailable: string };\n readonly assertions: readonly AssertionOutcome[] | { readonly unavailable: string };\n /** Section names whose assertions are premises. Compared without case, emphasis, a leading number or a trailing colon. */\n readonly premiseSections: readonly string[];\n readonly unverifiedRulings: readonly { readonly id: string; readonly reason: string }[];\n /** The rulings whose signatures verify, which a protected file the archive refuses may be covered by. */\n readonly verifiedRulings: readonly VerifiedRuling[];\n /** Whether spec-brief's configuration loads this package's plugin, through which its archive learns of them. */\n readonly pluginLoaded: boolean;\n}\n\nexport interface AuditReport {\n readonly findings: readonly Finding[];\n readonly counts: Readonly<Record<Severity, number>>;\n}\n\nfunction finding(rule: string, severity: Severity, message: string, hint: string, file?: string, line?: number): Finding {\n return { rule, severity, message, hint, ...(file === undefined ? {} : { file }), ...(line === undefined ? {} : { line }) };\n}\n\n/**\n * Whether an assertion's section is a premise section. Compared as the\n * rulings section is, and as spec-brief compares section names: a heading\n * written `## 3. **The Defect, Measured:**` is still the premise section, and\n * an assertion under it read as a goal would fail the round for retiring it.\n */\nexport function isPremise(sections: string | null | readonly string[], premises: readonly string[]): boolean {\n const list = sections === null ? [] : typeof sections === 'string' ? [sections] : sections;\n // A premise section's subsections are still premises: `### Before` under\n // `## The Defect, Measured` states what was true before the round.\n return list.some((section) => premises.some((premise) => sameSection(section, premise)));\n}\n\n/**\n * A live brief's premise that no longer holds, as `premises` reports it. On\n * the brief a round is working on - the one the flag, SPEC_BRIEF or the\n * branch names - that is what the round set out to do, and it is reported as\n * the audit reports it; on any other, what the brief was written against has\n * changed under it.\n */\nexport function premiseFinding(\n brief: Pick<BriefRow, 'id' | 'file'>,\n outcome: { readonly description: string; readonly message: string; readonly line: number },\n active: boolean,\n): Finding {\n if (active) {\n return finding(\n 'premise-retired',\n 'note',\n `brief ${brief.id}'s premise no longer holds, as the round on it intends: ${outcome.description}`,\n 'nothing to do: this is the round that changes it, and audit measures it',\n brief.file,\n outcome.line,\n );\n }\n return finding(\n 'stale-premise',\n 'error',\n `brief ${brief.id}'s premise no longer holds: ${outcome.description}: ${outcome.message}`,\n 'what the brief was written against has changed; archive the brief if its work is done, or rewrite its premise before a round is run on it',\n brief.file,\n outcome.line,\n );\n}\n\n/**\n * The next step for a reason the archive gives. A protected file a verified\n * ruling covers is refused only because the archive did not learn of the\n * ruling, and recording a departure in the brief would not fix that.\n */\nfunction archiveHint(reason: ArchiveReason, input: AuditInput): string {\n const ruling = reason.rule === 'protected-file' && reason.path !== undefined ? rulingFor(input.verifiedRulings, reason.path) : undefined;\n if (ruling === undefined) return reason.hint;\n const allowed = `ruling ${ruling.id}, signed by ${ruling.signer}, allows it`;\n return input.pluginLoaded\n ? `${allowed}, and the archive still refused it: check that spec-brief loads the spec-harness installed here and measures from the same base (spec-harness doctor)`\n : `${allowed}, but spec-brief does not load spec-harness's plugin, which is how its archive learns of signed rulings: add \"${PLUGIN}\" to \"plugins\" in its configuration, or run spec-harness init --write`;\n}\n\nexport function audit(input: AuditInput): AuditReport {\n const { brief } = input;\n const out: Finding[] = [];\n\n if (input.unmeasured !== null) {\n out.push(\n finding(\n 'unmeasured',\n 'warning',\n `the round's changes were not measured: ${input.unmeasured}`,\n 'pass --base <ref>, set \"base\" in .spec-harness.json, or audit on the round\\'s branch before it merges',\n brief.file,\n ),\n );\n }\n\n if ('unavailable' in input.archive) {\n out.push(finding('archive-unchecked', 'warning', `what the archive would say is unknown: ${input.archive.unavailable}`, 'install spec-brief, or fix what stopped it', brief.file));\n } else {\n for (const reason of [...input.archive.blocking, ...input.archive.warnings]) {\n out.push(finding(`archive/${reason.rule}`, reason.severity, reason.message, archiveHint(reason, input), reason.file, reason.line));\n }\n }\n\n if (!Array.isArray(input.assertions)) {\n const unavailable = (input.assertions as { unavailable: string }).unavailable;\n out.push(finding('assertions-unchecked', 'warning', `the brief's assertions were not run: ${unavailable}`, 'install spec-guard, or fix what stopped it', brief.file));\n } else {\n for (const outcome of input.assertions) {\n const premise = isPremise(outcome.enclosing ?? outcome.section, input.premiseSections);\n if (premise && outcome.ok) {\n out.push(\n finding(\n 'premise-holds',\n 'warning',\n `a premise still holds after the round: ${outcome.description}`,\n 'the round set out to change what this premise states; check that it did, or move the assertion out of the premises',\n brief.file,\n outcome.line,\n ),\n );\n } else if (premise) {\n out.push(finding('premise-retired', 'note', `a premise no longer holds, as the round intended: ${outcome.description}`, 'nothing to do', brief.file, outcome.line));\n } else if (!outcome.ok) {\n out.push(finding('goal-failed', 'error', `${outcome.description}: ${outcome.message}`, 'the round is not done until this holds', brief.file, outcome.line));\n }\n }\n }\n\n for (const ruling of input.unverifiedRulings) {\n out.push(\n finding(\n 'ruling-unverified',\n 'warning',\n `ruling ${ruling.id} allows nothing: ${ruling.reason}`,\n 'a ruling counts when the commit that last changed its row is signed by a key in the base branch\\'s allowed signers',\n brief.file,\n ),\n );\n }\n\n for (const file of input.dependencies.unread) {\n out.push(finding('manifest-unread', 'warning', `${file} changed and could not be read for dependencies`, 'check its dependencies by hand; the audit cannot', file));\n }\n for (const change of input.dependencies.changes) {\n const where = `${change.file} (${change.section})`;\n if (change.before === null) {\n out.push(\n finding(\n 'new-dependency',\n 'warning',\n `the round added ${change.ecosystem} dependency \"${change.name}\"${change.after === '' ? '' : ` ${change.after}`} in ${where}`,\n 'say in the brief why it is needed, or remove it; a new dependency is code no reviewer read',\n change.file,\n ),\n );\n } else if (change.after === null) {\n out.push(finding('dependency-removed', 'note', `the round removed \"${change.name}\" from ${where}`, 'nothing to do', change.file));\n } else {\n out.push(finding('dependency-changed', 'note', `the round moved \"${change.name}\" from ${change.before} to ${change.after} in ${where}`, 'nothing to do, if the brief meant it', change.file));\n }\n }\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, note: 0 };\n for (const item of out) counts[item.severity] += 1;\n return { findings: out, counts };\n}\n"]}
|
package/dist/cli.js
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';
|
|
11
11
|
import { ConfigError, SIBLINGS } from './config.js';
|
|
12
|
-
import {
|
|
12
|
+
import { describeBase, describePlugin, describeSigners } from './configure.js';
|
|
13
|
+
import { show, stagedChanges } from './git.js';
|
|
13
14
|
import { claudeResponse, gitResponse, parseClaudeHook } from './hooks.js';
|
|
14
15
|
import { premisesCommand } from './premises.js';
|
|
15
16
|
import { createReader } from './reader.js';
|
|
16
|
-
import { checkPaths } from './round.js';
|
|
17
|
+
import { checkPaths, resolveBase, specBriefPlugin } from './round.js';
|
|
17
18
|
import { mcpCommand } from './server.js';
|
|
18
19
|
import { initCommand } from './setup.js';
|
|
19
20
|
import { SiblingError } from './siblings.js';
|
|
@@ -67,7 +68,7 @@ async function decisionsFor(workspace, options, io, given, cwd) {
|
|
|
67
68
|
const { brief, note, problem } = describeActive(active);
|
|
68
69
|
if (problem !== null)
|
|
69
70
|
return { decisions: [], problem };
|
|
70
|
-
const decisions = await checkPaths(workspace, brief, note, given, cwd, reader);
|
|
71
|
+
const decisions = await checkPaths(workspace, brief, note, given, cwd, reader, options.base);
|
|
71
72
|
return { decisions, problem: null };
|
|
72
73
|
}
|
|
73
74
|
const SYMBOL = { allow: 'ok ', warn: 'warning', ask: 'ask ', deny: 'refused' };
|
|
@@ -177,11 +178,35 @@ async function doctorCommand(options, io) {
|
|
|
177
178
|
return { ...row, version: sibling.version, detail };
|
|
178
179
|
});
|
|
179
180
|
const named = namedId(workspace, options, io.env);
|
|
181
|
+
// A ruling counts only by a signature checked against the allowed signers
|
|
182
|
+
// on the base, and only once spec-brief's archive asks the plugin about it.
|
|
183
|
+
const base = await resolveBase(workspace, options.base);
|
|
184
|
+
const signersFile = workspace.config.rulings.allowedSigners;
|
|
185
|
+
const onBase = base.kind === 'resolved' ? (await show(base.sha, signersFile, workspace.root)) !== null : null;
|
|
186
|
+
const signers = describeSigners(signersFile, base.kind === 'resolved' ? base.ref : null, onBase === true);
|
|
187
|
+
const plugin = await specBriefPlugin(workspace.root);
|
|
180
188
|
if (options.format === 'json') {
|
|
181
|
-
io.stdout.write(json('doctor', {
|
|
189
|
+
io.stdout.write(json('doctor', {
|
|
190
|
+
root: workspace.root,
|
|
191
|
+
branch: workspace.branch,
|
|
192
|
+
brief: named,
|
|
193
|
+
base: base.kind === 'resolved' ? { ref: base.ref, source: base.source, mergeBase: base.mergeBase } : { unresolved: base.reason },
|
|
194
|
+
allowedSigners: { file: signersFile, onBase, detail: signers },
|
|
195
|
+
plugin: { state: plugin.kind, file: 'file' in plugin ? plugin.file : null, detail: describePlugin(plugin) },
|
|
196
|
+
siblings: rows,
|
|
197
|
+
}));
|
|
182
198
|
}
|
|
183
199
|
else {
|
|
184
|
-
io.stdout.write(
|
|
200
|
+
io.stdout.write([
|
|
201
|
+
`root ${workspace.root}`,
|
|
202
|
+
`branch ${workspace.branch ?? '(detached)'}`,
|
|
203
|
+
`brief ${named ?? '(none named)'}`,
|
|
204
|
+
`base ${describeBase(base.kind === 'resolved' ? base : { reason: base.reason })}`,
|
|
205
|
+
`signers ${signers}`,
|
|
206
|
+
`plugin ${describePlugin(plugin)}`,
|
|
207
|
+
'',
|
|
208
|
+
'',
|
|
209
|
+
].join('\n'));
|
|
185
210
|
for (const row of rows)
|
|
186
211
|
io.stdout.write(`${row.state.padEnd(8)} ${row.tool.padEnd(10)} ${row.detail}\n`);
|
|
187
212
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The command line: arguments, dispatch, output and exit codes.\n *\n * Exit 0: done, nothing refused. Exit 1: something refused or found. Exit 2:\n * the answer cannot be trusted - a bad flag, a configuration that does not\n * load, a sibling that is missing or printed something unreadable. A run that\n * could not look must never exit as though it looked and found nothing\n * (spec-core ADR-0005).\n */\n\nimport { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';\nimport { ConfigError, SIBLINGS } from './config.js';\nimport { stagedChanges } from './git.js';\nimport type { Decision } from './guard.js';\nimport { claudeResponse, gitResponse, parseClaudeHook } from './hooks.js';\nimport { premisesCommand } from './premises.js';\nimport { createReader } from './reader.js';\nimport { checkPaths } from './round.js';\nimport { mcpCommand } from './server.js';\nimport { initCommand } from './setup.js';\nimport { SiblingError } from './siblings.js';\nimport { MINIMUM_VERSIONS } from './versions.js';\nimport {\n activeBrief,\n describeActive,\n EXIT_ERROR,\n EXIT_FAILED,\n EXIT_OK,\n json,\n namedId,\n openWorkspace,\n parseOptions,\n UsageError,\n version,\n type CliIO,\n type Options,\n type Workspace,\n} from './workspace.js';\n\nexport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';\n\nexport const HELP = `spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies.\n 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 is stale.\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, and which\n brief is named. Exit 1 when one is older than this release needs.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch.\n --root <dir> Run from another directory.\n --format <fmt> pretty or json.\n --strict Warnings fail the run.\n --help, --version\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n`;\n\n\n/* ------------------------------------------------------------------- guard */\n\nconst reader = createReader();\n\nasync function decisionsFor(\n workspace: Workspace,\n options: Options,\n io: CliIO,\n given: readonly string[],\n cwd: string,\n): Promise<{ decisions: Decision[]; problem: string | null }> {\n const active = await activeBrief(workspace, options, io.env);\n const { brief, note, problem } = describeActive(active);\n if (problem !== null) return { decisions: [], problem };\n const decisions = await checkPaths(workspace, brief, note, given, cwd, reader);\n return { decisions, problem: null };\n}\n\nconst SYMBOL: Record<Decision['verdict'], string> = { allow: 'ok ', warn: 'warning', ask: 'ask ', deny: 'refused' };\n\nasync function guardCommand(options: Options, io: CliIO): Promise<number> {\n if (options.positionals.length === 0) throw new UsageError('guard needs at least one path');\n const workspace = await openWorkspace(options, io);\n const { decisions, problem } = await decisionsFor(workspace, options, io, options.positionals, io.cwd);\n if (problem !== null) {\n io.stderr.write(`spec-harness: ${problem}\\n`);\n return EXIT_ERROR;\n }\n if (options.format === 'json') {\n io.stdout.write(json('guard', { decisions }));\n } else {\n for (const decision of decisions) {\n io.stdout.write(`${SYMBOL[decision.verdict]} ${decision.message}\\n`);\n if (decision.verdict !== 'allow') io.stdout.write(` ${decision.hint}\\n`);\n }\n }\n const refused = decisions.some((d) => d.verdict === 'deny' || d.verdict === 'ask');\n const warned = decisions.some((d) => d.verdict === 'warn');\n return refused || (options.strict && warned) ? EXIT_FAILED : EXIT_OK;\n}\n\n/* -------------------------------------------------------------------- hook */\n\nasync function readStdin(io: CliIO): Promise<string> {\n if (io.stdin !== undefined) return io.stdin();\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(chunk as Buffer);\n return Buffer.concat(chunks).toString('utf8');\n}\n\nasync function hookCommand(options: Options, io: CliIO): Promise<number> {\n const kind = options.positionals[0];\n if (kind === 'claude') {\n const request = parseClaudeHook(await readStdin(io));\n if ('error' in request) {\n // A hook that cannot read its question refuses nothing it cannot see,\n // but says so where the person reads it: a non-blocking error.\n io.stderr.write(`spec-harness: ${request.error}\\n`);\n return EXIT_FAILED;\n }\n if (request.paths.length === 0) return EXIT_OK;\n const cwd = request.cwd ?? io.cwd;\n let workspace: Workspace;\n try {\n workspace = await openWorkspace({ ...options, root: options.root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);\n } catch (error) {\n if (error instanceof UsageError) return EXIT_OK;\n throw error;\n }\n try {\n const { decisions, problem } = await decisionsFor(workspace, options, io, request.paths, cwd);\n if (problem !== null) {\n // Exit 2 blocks the tool call, and its stderr reaches the model.\n io.stderr.write(`spec-harness: ${problem}. Fix the brief or the branch before writing.\\n`);\n return EXIT_ERROR;\n }\n const response = claudeResponse(request.event, decisions);\n io.stdout.write(response.stdout);\n return response.exitCode;\n } catch (error) {\n if (request.event !== 'PreToolUse') throw error;\n // A round is named and its briefs cannot be read: the guard cannot\n // tell a protected file from any other, so the write waits.\n io.stderr.write(`spec-harness: cannot check this write: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n if (kind === 'git') {\n const workspace = await openWorkspace(options, io);\n const staged = await stagedChanges(workspace.root);\n const paths = staged.flatMap((change) => (change.from === undefined ? [change.path] : [change.from, change.path]));\n if (paths.length === 0) return EXIT_OK;\n const { decisions, problem } = await decisionsFor(workspace, options, io, paths, workspace.root);\n if (problem !== null) {\n io.stderr.write(`spec-harness: ${problem}\\n`);\n return EXIT_ERROR;\n }\n const response = gitResponse(decisions);\n io.stderr.write(response.text);\n return response.exitCode;\n }\n throw new UsageError('hook takes \"claude\" or \"git\"');\n}\n\n/* ------------------------------------------------------------------ doctor */\n\nasync function doctorCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const rows = SIBLINGS.map((name) => {\n const sibling = workspace.siblings.locate(name);\n const row = { tool: name, state: sibling.kind, minimum: MINIMUM_VERSIONS[name] };\n if (sibling.kind === 'absent') return { ...row, version: null, detail: sibling.reason };\n if (sibling.kind === 'outdated') return { ...row, version: sibling.version, detail: sibling.reason };\n const command = sibling.command.join(' ');\n const detail = sibling.version === null ? `${command} (named in .spec-harness.json; its version is not checked)` : `${command} (${sibling.version})`;\n return { ...row, version: sibling.version, detail };\n });\n const named = namedId(workspace, options, io.env);\n if (options.format === 'json') {\n io.stdout.write(json('doctor', { root: workspace.root, branch: workspace.branch, brief: named, siblings: rows }));\n } else {\n io.stdout.write(`root ${workspace.root}\\nbranch ${workspace.branch ?? '(detached)'}\\nbrief ${named ?? '(none named)'}\\n\\n`);\n for (const row of rows) io.stdout.write(`${row.state.padEnd(8)} ${row.tool.padEnd(10)} ${row.detail}\\n`);\n }\n // An outdated sibling is a problem to fix, where a missing optional one is\n // a choice; with none installed, the harness can check nothing.\n const outdated = rows.some((row) => row.state === 'outdated');\n const usable = rows.some((row) => row.state === 'found');\n return outdated || !usable ? EXIT_FAILED : EXIT_OK;\n}\n\n/* ---------------------------------------------------------------- dispatch */\n\nconst COMMANDS: Readonly<Record<string, (options: Options, io: CliIO) => Promise<number>>> = {\n guard: guardCommand,\n hook: hookCommand,\n context: contextCommand,\n audit: auditCommand,\n escalate: escalateCommand,\n rule: ruleCommand,\n rulings: rulingsCommand,\n probe: probeCommand,\n premises: premisesCommand,\n init: initCommand,\n mcp: mcpCommand,\n doctor: doctorCommand,\n};\n\nexport async function run(argv: readonly string[], io: CliIO): Promise<number> {\n let options: Options;\n try {\n options = parseOptions(argv);\n } catch (error) {\n io.stderr.write(`spec-harness: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n if (options.version) {\n io.stdout.write(`${version()}\\n`);\n return EXIT_OK;\n }\n if (options.help || options.command === undefined) {\n io.stdout.write(HELP);\n return options.command === undefined && !options.help ? EXIT_ERROR : EXIT_OK;\n }\n const command = COMMANDS[options.command];\n if (command === undefined) {\n io.stderr.write(`spec-harness: unknown command \"${options.command}\"; see spec-harness --help\\n`);\n return EXIT_ERROR;\n }\n try {\n return await command(options, io);\n } catch (error) {\n if (error instanceof UsageError || error instanceof ConfigError || error instanceof SiblingError) {\n io.stderr.write(`spec-harness: ${error.message}\\n`);\n return EXIT_ERROR;\n }\n throw error;\n }\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n return run(argv, { stdout: process.stdout, stderr: process.stderr, cwd: process.cwd(), env: process.env });\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The command line: arguments, dispatch, output and exit codes.\n *\n * Exit 0: done, nothing refused. Exit 1: something refused or found. Exit 2:\n * the answer cannot be trusted - a bad flag, a configuration that does not\n * load, a sibling that is missing or printed something unreadable. A run that\n * could not look must never exit as though it looked and found nothing\n * (spec-core ADR-0005).\n */\n\nimport { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';\nimport { ConfigError, SIBLINGS } from './config.js';\nimport { describeBase, describePlugin, describeSigners } from './configure.js';\nimport { show, stagedChanges } from './git.js';\nimport type { Decision } from './guard.js';\nimport { claudeResponse, gitResponse, parseClaudeHook } from './hooks.js';\nimport { premisesCommand } from './premises.js';\nimport { createReader } from './reader.js';\nimport { checkPaths, resolveBase, specBriefPlugin } from './round.js';\nimport { mcpCommand } from './server.js';\nimport { initCommand } from './setup.js';\nimport { SiblingError } from './siblings.js';\nimport { MINIMUM_VERSIONS } from './versions.js';\nimport {\n activeBrief,\n describeActive,\n EXIT_ERROR,\n EXIT_FAILED,\n EXIT_OK,\n json,\n namedId,\n openWorkspace,\n parseOptions,\n UsageError,\n version,\n type CliIO,\n type Options,\n type Workspace,\n} from './workspace.js';\n\nexport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';\n\nexport const HELP = `spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. It counts once committed signed.\n rulings [brief] The brief's rulings, and whether each one's signature verifies.\n audit [brief] Did the round stay inside the lines: what the archive would say,\n the brief's assertions, unverified rulings, new dependencies.\n 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 is stale.\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, and which\n brief is named. Exit 1 when one is older than this release needs.\n\nOptions:\n --brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.\n --base <ref> What the round is measured from. Otherwise \"base\" in\n .spec-harness.json, then the remote's default branch.\n --root <dir> Run from another directory.\n --format <fmt> pretty or json.\n --strict Warnings fail the run.\n --help, --version\n\nExit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.\n`;\n\n\n/* ------------------------------------------------------------------- guard */\n\nconst reader = createReader();\n\nasync function decisionsFor(\n workspace: Workspace,\n options: Options,\n io: CliIO,\n given: readonly string[],\n cwd: string,\n): Promise<{ decisions: Decision[]; problem: string | null }> {\n const active = await activeBrief(workspace, options, io.env);\n const { brief, note, problem } = describeActive(active);\n if (problem !== null) return { decisions: [], problem };\n const decisions = await checkPaths(workspace, brief, note, given, cwd, reader, options.base);\n return { decisions, problem: null };\n}\n\nconst SYMBOL: Record<Decision['verdict'], string> = { allow: 'ok ', warn: 'warning', ask: 'ask ', deny: 'refused' };\n\nasync function guardCommand(options: Options, io: CliIO): Promise<number> {\n if (options.positionals.length === 0) throw new UsageError('guard needs at least one path');\n const workspace = await openWorkspace(options, io);\n const { decisions, problem } = await decisionsFor(workspace, options, io, options.positionals, io.cwd);\n if (problem !== null) {\n io.stderr.write(`spec-harness: ${problem}\\n`);\n return EXIT_ERROR;\n }\n if (options.format === 'json') {\n io.stdout.write(json('guard', { decisions }));\n } else {\n for (const decision of decisions) {\n io.stdout.write(`${SYMBOL[decision.verdict]} ${decision.message}\\n`);\n if (decision.verdict !== 'allow') io.stdout.write(` ${decision.hint}\\n`);\n }\n }\n const refused = decisions.some((d) => d.verdict === 'deny' || d.verdict === 'ask');\n const warned = decisions.some((d) => d.verdict === 'warn');\n return refused || (options.strict && warned) ? EXIT_FAILED : EXIT_OK;\n}\n\n/* -------------------------------------------------------------------- hook */\n\nasync function readStdin(io: CliIO): Promise<string> {\n if (io.stdin !== undefined) return io.stdin();\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(chunk as Buffer);\n return Buffer.concat(chunks).toString('utf8');\n}\n\nasync function hookCommand(options: Options, io: CliIO): Promise<number> {\n const kind = options.positionals[0];\n if (kind === 'claude') {\n const request = parseClaudeHook(await readStdin(io));\n if ('error' in request) {\n // A hook that cannot read its question refuses nothing it cannot see,\n // but says so where the person reads it: a non-blocking error.\n io.stderr.write(`spec-harness: ${request.error}\\n`);\n return EXIT_FAILED;\n }\n if (request.paths.length === 0) return EXIT_OK;\n const cwd = request.cwd ?? io.cwd;\n let workspace: Workspace;\n try {\n workspace = await openWorkspace({ ...options, root: options.root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);\n } catch (error) {\n if (error instanceof UsageError) return EXIT_OK;\n throw error;\n }\n try {\n const { decisions, problem } = await decisionsFor(workspace, options, io, request.paths, cwd);\n if (problem !== null) {\n // Exit 2 blocks the tool call, and its stderr reaches the model.\n io.stderr.write(`spec-harness: ${problem}. Fix the brief or the branch before writing.\\n`);\n return EXIT_ERROR;\n }\n const response = claudeResponse(request.event, decisions);\n io.stdout.write(response.stdout);\n return response.exitCode;\n } catch (error) {\n if (request.event !== 'PreToolUse') throw error;\n // A round is named and its briefs cannot be read: the guard cannot\n // tell a protected file from any other, so the write waits.\n io.stderr.write(`spec-harness: cannot check this write: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n if (kind === 'git') {\n const workspace = await openWorkspace(options, io);\n const staged = await stagedChanges(workspace.root);\n const paths = staged.flatMap((change) => (change.from === undefined ? [change.path] : [change.from, change.path]));\n if (paths.length === 0) return EXIT_OK;\n const { decisions, problem } = await decisionsFor(workspace, options, io, paths, workspace.root);\n if (problem !== null) {\n io.stderr.write(`spec-harness: ${problem}\\n`);\n return EXIT_ERROR;\n }\n const response = gitResponse(decisions);\n io.stderr.write(response.text);\n return response.exitCode;\n }\n throw new UsageError('hook takes \"claude\" or \"git\"');\n}\n\n/* ------------------------------------------------------------------ doctor */\n\nasync function doctorCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const rows = SIBLINGS.map((name) => {\n const sibling = workspace.siblings.locate(name);\n const row = { tool: name, state: sibling.kind, minimum: MINIMUM_VERSIONS[name] };\n if (sibling.kind === 'absent') return { ...row, version: null, detail: sibling.reason };\n if (sibling.kind === 'outdated') return { ...row, version: sibling.version, detail: sibling.reason };\n const command = sibling.command.join(' ');\n const detail = sibling.version === null ? `${command} (named in .spec-harness.json; its version is not checked)` : `${command} (${sibling.version})`;\n return { ...row, version: sibling.version, detail };\n });\n const named = namedId(workspace, options, io.env);\n // A ruling counts only by a signature checked against the allowed signers\n // on the base, and only once spec-brief's archive asks the plugin about it.\n const base = await resolveBase(workspace, options.base);\n const signersFile = workspace.config.rulings.allowedSigners;\n const onBase = base.kind === 'resolved' ? (await show(base.sha, signersFile, workspace.root)) !== null : null;\n const signers = describeSigners(signersFile, base.kind === 'resolved' ? base.ref : null, onBase === true);\n const plugin = await specBriefPlugin(workspace.root);\n if (options.format === 'json') {\n io.stdout.write(\n json('doctor', {\n root: workspace.root,\n branch: workspace.branch,\n brief: named,\n base: base.kind === 'resolved' ? { ref: base.ref, source: base.source, mergeBase: base.mergeBase } : { unresolved: base.reason },\n allowedSigners: { file: signersFile, onBase, detail: signers },\n plugin: { state: plugin.kind, file: 'file' in plugin ? plugin.file : null, detail: describePlugin(plugin) },\n siblings: rows,\n }),\n );\n } else {\n io.stdout.write(\n [\n `root ${workspace.root}`,\n `branch ${workspace.branch ?? '(detached)'}`,\n `brief ${named ?? '(none named)'}`,\n `base ${describeBase(base.kind === 'resolved' ? base : { reason: base.reason })}`,\n `signers ${signers}`,\n `plugin ${describePlugin(plugin)}`,\n '',\n '',\n ].join('\\n'),\n );\n for (const row of rows) io.stdout.write(`${row.state.padEnd(8)} ${row.tool.padEnd(10)} ${row.detail}\\n`);\n }\n // An outdated sibling is a problem to fix, where a missing optional one is\n // a choice; with none installed, the harness can check nothing.\n const outdated = rows.some((row) => row.state === 'outdated');\n const usable = rows.some((row) => row.state === 'found');\n return outdated || !usable ? EXIT_FAILED : EXIT_OK;\n}\n\n/* ---------------------------------------------------------------- dispatch */\n\nconst COMMANDS: Readonly<Record<string, (options: Options, io: CliIO) => Promise<number>>> = {\n guard: guardCommand,\n hook: hookCommand,\n context: contextCommand,\n audit: auditCommand,\n escalate: escalateCommand,\n rule: ruleCommand,\n rulings: rulingsCommand,\n probe: probeCommand,\n premises: premisesCommand,\n init: initCommand,\n mcp: mcpCommand,\n doctor: doctorCommand,\n};\n\nexport async function run(argv: readonly string[], io: CliIO): Promise<number> {\n let options: Options;\n try {\n options = parseOptions(argv);\n } catch (error) {\n io.stderr.write(`spec-harness: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n if (options.version) {\n io.stdout.write(`${version()}\\n`);\n return EXIT_OK;\n }\n if (options.help || options.command === undefined) {\n io.stdout.write(HELP);\n return options.command === undefined && !options.help ? EXIT_ERROR : EXIT_OK;\n }\n const command = COMMANDS[options.command];\n if (command === undefined) {\n io.stderr.write(`spec-harness: unknown command \"${options.command}\"; see spec-harness --help\\n`);\n return EXIT_ERROR;\n }\n try {\n return await command(options, io);\n } catch (error) {\n if (error instanceof UsageError || error instanceof ConfigError || error instanceof SiblingError) {\n io.stderr.write(`spec-harness: ${error.message}\\n`);\n return EXIT_ERROR;\n }\n throw error;\n }\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n return run(argv, { stdout: process.stdout, stderr: process.stderr, cwd: process.cwd(), env: process.env });\n}\n\n"]}
|
package/dist/commands.js
CHANGED
|
@@ -39,7 +39,7 @@ function printFindings(io, findings) {
|
|
|
39
39
|
export async function contextCommand(options, io) {
|
|
40
40
|
const workspace = await openWorkspace(options, io);
|
|
41
41
|
const { brief, briefs } = await targetBrief(workspace, options, io, options.positionals[0]);
|
|
42
|
-
const packet = await buildContext(workspace, brief, briefs, reader);
|
|
42
|
+
const packet = await buildContext(workspace, brief, briefs, reader, options.base);
|
|
43
43
|
if (options.format === 'json') {
|
|
44
44
|
io.stdout.write(json('context', { brief: brief.id, markdown: packet.markdown, included: packet.included, omitted: packet.omitted, unresolved: packet.unresolved }));
|
|
45
45
|
}
|