@descent-vtt/spec-harness 0.7.0 → 0.9.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 +54 -0
- package/README.md +5 -3
- package/dist/answers.d.ts +54 -0
- package/dist/answers.js +139 -0
- package/dist/answers.js.map +1 -0
- package/dist/branch.d.ts +2 -2
- package/dist/branch.js +3 -1
- package/dist/branch.js.map +1 -1
- package/dist/cli.js +28 -17
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +7 -2
- package/dist/commands.js.map +1 -1
- package/dist/configure.d.ts +13 -1
- package/dist/configure.js +46 -3
- package/dist/configure.js.map +1 -1
- package/dist/context.d.ts +16 -4
- package/dist/context.js +27 -12
- package/dist/context.js.map +1 -1
- package/dist/document.d.ts +15 -3
- package/dist/document.js.map +1 -1
- package/dist/fs.js +8 -5
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +12 -6
- package/dist/git.js +25 -11
- package/dist/git.js.map +1 -1
- package/dist/host.d.ts +4 -3
- package/dist/host.js +74 -13
- package/dist/host.js.map +1 -1
- package/dist/plugin.js +10 -17
- package/dist/plugin.js.map +1 -1
- package/dist/premises.js +16 -12
- package/dist/premises.js.map +1 -1
- package/dist/reader.js +27 -10
- package/dist/reader.js.map +1 -1
- package/dist/round.d.ts +1 -1
- package/dist/round.js +61 -55
- package/dist/round.js.map +1 -1
- package/dist/server.js +17 -6
- package/dist/server.js.map +1 -1
- package/dist/setup.js +20 -15
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +2 -0
- package/dist/siblings.js +12 -4
- package/dist/siblings.js.map +1 -1
- package/dist/signers.d.ts +8 -5
- package/dist/signers.js +126 -51
- package/dist/signers.js.map +1 -1
- package/dist/versions.d.ts +16 -3
- package/dist/versions.js +12 -2
- package/dist/versions.js.map +1 -1
- package/dist/workspace.d.ts +16 -3
- package/dist/workspace.js +12 -2
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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,60 @@ 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.9.0
|
|
13
|
+
|
|
14
|
+
spec-harness refuses sibling JSON of a shape it does not read, with exit 2,
|
|
15
|
+
where it stopped with a stack trace or read the document halfway, and two
|
|
16
|
+
messages name what is really missing.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- On a branch with nothing committed yet, `audit`, `context`, `doctor`
|
|
21
|
+
and the MCP tools say HEAD names no commit, where they said the base named
|
|
22
|
+
none.
|
|
23
|
+
- When spec-brief exits 2 while listing the briefs and says nothing, the
|
|
24
|
+
error ends in `exit 2`, where it ended with the colon before a reason.
|
|
25
|
+
- When spec-guard or spec-brief prints JSON of a shape spec-harness does
|
|
26
|
+
not read - `results` that is not a list, a result whose `ok` is not true
|
|
27
|
+
or false, an archive reason of a severity it does not know - `audit` and
|
|
28
|
+
`premises` exit 2 naming the tool and the field, where they stopped with
|
|
29
|
+
a stack trace or read the document halfway; `context` says the rules could
|
|
30
|
+
not be read and why, as it does for spec-guard output that is not JSON
|
|
31
|
+
([ADR-0002](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0002-siblings-through-their-command-lines.md)).
|
|
32
|
+
|
|
33
|
+
## 0.8.0
|
|
34
|
+
|
|
35
|
+
The context packet names front matter lines it could not read, `doctor`
|
|
36
|
+
reads the release of a Claude Code installed with npm on Windows, and
|
|
37
|
+
spec-harness reads status words in English only again.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- `context` names each line of a cited document's front matter that is not
|
|
42
|
+
`key: value`, with its line number and reason, and says a status on it was
|
|
43
|
+
not read; `--format json` and `start_round` gain `unreadableFrontMatter`
|
|
44
|
+
([ADR-0001](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0001-the-harness-decides-the-agent-writes.md)).
|
|
45
|
+
|
|
46
|
+
### Removed
|
|
47
|
+
|
|
48
|
+
- `context` no longer reads a cited document's status under `狀態` or
|
|
49
|
+
`状态`. Upgrading: write the status in English, e.g. `status: superseded`.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- `doctor` reads the release of an npm-installed Claude Code on Windows from
|
|
54
|
+
the package its `claude.cmd` shim runs, without running the shim, where it
|
|
55
|
+
could not tell and failed `--strict`
|
|
56
|
+
([ADR-0012](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0012-one-way-into-claude-code.md)).
|
|
57
|
+
- `doctor` says it cannot tell the release of a `claude.exe` Windows cannot
|
|
58
|
+
start, where it stopped with `spawn UNKNOWN`.
|
|
59
|
+
- `doctor` reads the allowed-signers file's quotes and options as OpenSSH
|
|
60
|
+
does, and notes a line OpenSSH refuses as no signer
|
|
61
|
+
([ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md)).
|
|
62
|
+
- `init` and `doctor` resolve the spec-brief plugin export as Node does: the
|
|
63
|
+
first matching condition in written order, and a `null` target as not
|
|
64
|
+
exported.
|
|
65
|
+
|
|
12
66
|
## 0.7.0
|
|
13
67
|
|
|
14
68
|
The audit and `premises` no longer pass on what they could not read, the
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ The packet an agent starts a round with: the brief in full - it is the contract
|
|
|
68
68
|
- the rulings already signed;
|
|
69
69
|
- the briefs it depends on, and whether they are done;
|
|
70
70
|
- the architecture rules spec-guard holds that code to;
|
|
71
|
-
- 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. Each is headed by its first level-one heading and the status its front matter gives under `status`,
|
|
71
|
+
- 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. Each is headed by its first level-one heading and the status its front matter gives under `status`, in English as every spec-* tool reads it, whatever language the document is written in.
|
|
72
72
|
|
|
73
73
|
`--base` names the base the rulings are verified against.
|
|
74
74
|
|
|
@@ -80,7 +80,7 @@ The packet an agent starts a round with: the brief in full - it is the contract
|
|
|
80
80
|
|
|
81
81
|
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.
|
|
82
82
|
|
|
83
|
-
**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. The
|
|
83
|
+
**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.
|
|
84
84
|
|
|
85
85
|
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:
|
|
86
86
|
|
|
@@ -298,10 +298,12 @@ The first thing to run when a hook refuses something unexpectedly. It reports:
|
|
|
298
298
|
- **The signers' keys**: a note naming each signer in the allowed-signers file whose key is not a FIDO2 key (`sk-ssh-ed25519@openssh.com` or `sk-ecdsa-sha2-nistp256@openssh.com`).
|
|
299
299
|
- A note, never a failure: [ADR-0006](https://github.com/DescentVTT/spec-harness/blob/main/docs/adr/0006-a-ruling-is-a-signed-commit.md) also accepts a key the agent's account cannot read, and a PIV or PKCS#11 hardware key reads as a plain `ssh-rsa` or `ecdsa` line.
|
|
300
300
|
- A `cert-authority` line is a certificate authority's key, not a person's, and is left out.
|
|
301
|
+
- A line OpenSSH refuses is named as no signer, with why. Its quotes are read as OpenSSH reads them: in the principals, the first double quote, wherever it opens, is taken out with the next, which ends the field; an option's value is always in double quotes, `\"` a quote inside it. And its options are OpenSSH's: `cert-authority`, `namespaces`, `valid-after` and `valid-before`, separated by commas.
|
|
301
302
|
- **How Claude Code runs the guard**: the Claude Code plugin, with the settings file that turns it on; `init`'s hooks and server; both, which guards every write twice and exits 1, with how to keep one; or neither.
|
|
302
303
|
- **Which Claude Code**, when Claude Code runs the guard: `claude --version`, found on `PATH` and run without a shell.
|
|
303
304
|
- Older than 2.1.139 exits 1. An older release ignores a hook's `args` and runs a bare `node`, which reads the hook's input as a script and fails, and a PreToolUse hook that fails with anything but exit 2 blocks nothing: every write passes unguarded.
|
|
304
|
-
-
|
|
305
|
+
- On Windows, npm installs Claude Code as a `claude.cmd` shim, which cannot be run without a shell, so it is read instead: the `node_modules\@anthropic-ai\claude-code` it runs, as npm's shim or pnpm's names it, and that package.json's `version`, which doctor says it read. Nothing is run.
|
|
306
|
+
- A `claude` that is not found, a Windows script that runs no `@anthropic-ai/claude-code` package whose package.json gives a version, and a version that cannot be read are *cannot tell*, never fine; `--strict` fails them.
|
|
305
307
|
- The Claude Code in an editor or the desktop app may be another release than the one on `PATH`: check it there with `claude --version` or `/status`.
|
|
306
308
|
- **git's pre-commit hook**: whether it runs spec-harness, where git runs it from: installed, missing, a hook of the repository's own without the line, or, outside Windows, one git skips because it is not executable.
|
|
307
309
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What spec-guard and spec-brief answer in JSON, besides the brief list
|
|
3
|
+
* briefs.ts reads: a query's rules, a run's results and errors, and an
|
|
4
|
+
* archive's plan. Each is checked for the fields the harness reads, with the
|
|
5
|
+
* types it reads them as, and a document of another shape is an error that
|
|
6
|
+
* names the tool and the field (ADR-0002): the command that asked stops with
|
|
7
|
+
* exit 2, as for any sibling that printed something it cannot read, rather
|
|
8
|
+
* than crash halfway through the document or count what it could not read as
|
|
9
|
+
* clean. Fields the harness does not read are not checked, so a sibling may
|
|
10
|
+
* add to its documents freely.
|
|
11
|
+
*/
|
|
12
|
+
import type { ArchiveReason } from './audit.js';
|
|
13
|
+
/** Where spec-guard places a directive. */
|
|
14
|
+
export interface Spec {
|
|
15
|
+
readonly file: string;
|
|
16
|
+
readonly line: number;
|
|
17
|
+
}
|
|
18
|
+
export interface QueryRule {
|
|
19
|
+
readonly document: string;
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
readonly reason: string | null;
|
|
24
|
+
/** Absent from a document that says nothing of it, which is in force. */
|
|
25
|
+
readonly inForce: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The rules in each of the results of `spec-guard query --json`, in order. */
|
|
28
|
+
export declare function readQueryRules(document: unknown): QueryRule[][];
|
|
29
|
+
export interface GuardResult {
|
|
30
|
+
readonly ok: boolean;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly spec?: Spec | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface GuardError {
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly raw: string;
|
|
38
|
+
readonly spec?: Spec | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */
|
|
41
|
+
export declare function readGuardRun(document: unknown): {
|
|
42
|
+
results: GuardResult[];
|
|
43
|
+
errors: GuardError[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* What `spec-brief archive --dry-run --format json` would refuse and warn
|
|
47
|
+
* about. A document with no plan plans nothing to refuse. spec-brief gives a
|
|
48
|
+
* reason without a hint where it has no next step, and the reason is carried
|
|
49
|
+
* as it gave it: a field it left out is undefined, which JSON leaves out too.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readArchivePlan(document: unknown): {
|
|
52
|
+
blocking: ArchiveReason[];
|
|
53
|
+
warnings: ArchiveReason[];
|
|
54
|
+
};
|
package/dist/answers.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What spec-guard and spec-brief answer in JSON, besides the brief list
|
|
3
|
+
* briefs.ts reads: a query's rules, a run's results and errors, and an
|
|
4
|
+
* archive's plan. Each is checked for the fields the harness reads, with the
|
|
5
|
+
* types it reads them as, and a document of another shape is an error that
|
|
6
|
+
* names the tool and the field (ADR-0002): the command that asked stops with
|
|
7
|
+
* exit 2, as for any sibling that printed something it cannot read, rather
|
|
8
|
+
* than crash halfway through the document or count what it could not read as
|
|
9
|
+
* clean. Fields the harness does not read are not checked, so a sibling may
|
|
10
|
+
* add to its documents freely.
|
|
11
|
+
*/
|
|
12
|
+
import { SiblingOutputError } from './briefs.js';
|
|
13
|
+
function isObject(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
/** Reads fields of one document, naming the tool, its command and the field in every refusal. */
|
|
17
|
+
function reader(printer) {
|
|
18
|
+
const refuse = (where, expected) => {
|
|
19
|
+
throw new SiblingOutputError(`${printer} printed ${where} that is not ${expected}`);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
object(value, where) {
|
|
23
|
+
return isObject(value) ? value : refuse(where, 'an object');
|
|
24
|
+
},
|
|
25
|
+
/** A list that is absent is an empty one. */
|
|
26
|
+
list(value, where) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return [];
|
|
29
|
+
return Array.isArray(value) ? value : refuse(where, 'a list');
|
|
30
|
+
},
|
|
31
|
+
text(value, where) {
|
|
32
|
+
return typeof value === 'string' ? value : refuse(where, 'text');
|
|
33
|
+
},
|
|
34
|
+
optionalText(value, where) {
|
|
35
|
+
return value === undefined ? undefined : typeof value === 'string' ? value : refuse(where, 'text');
|
|
36
|
+
},
|
|
37
|
+
number(value, where) {
|
|
38
|
+
return typeof value === 'number' ? value : refuse(where, 'a number');
|
|
39
|
+
},
|
|
40
|
+
optionalNumber(value, where) {
|
|
41
|
+
return value === undefined ? undefined : typeof value === 'number' ? value : refuse(where, 'a number');
|
|
42
|
+
},
|
|
43
|
+
flag(value, where) {
|
|
44
|
+
return typeof value === 'boolean' ? value : refuse(where, 'true or false');
|
|
45
|
+
},
|
|
46
|
+
refuse,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The rules in each of the results of `spec-guard query --json`, in order. */
|
|
50
|
+
export function readQueryRules(document) {
|
|
51
|
+
const read = reader('spec-guard query');
|
|
52
|
+
const top = read.object(document, 'a document');
|
|
53
|
+
return read.list(top['results'], 'results').map((value, index) => {
|
|
54
|
+
const result = read.object(value, `results[${index}]`);
|
|
55
|
+
return read.list(result['rules'], `results[${index}].rules`).map((ruleValue, at) => {
|
|
56
|
+
const where = `results[${index}].rules[${at}]`;
|
|
57
|
+
const rule = read.object(ruleValue, where);
|
|
58
|
+
const reason = rule['reason'];
|
|
59
|
+
if (reason !== undefined && reason !== null && typeof reason !== 'string')
|
|
60
|
+
read.refuse(`${where}.reason`, 'text or null');
|
|
61
|
+
const inForce = rule['inForce'];
|
|
62
|
+
return {
|
|
63
|
+
document: read.text(rule['document'], `${where}.document`),
|
|
64
|
+
line: read.number(rule['line'], `${where}.line`),
|
|
65
|
+
kind: read.text(rule['kind'], `${where}.kind`),
|
|
66
|
+
description: read.text(rule['description'], `${where}.description`),
|
|
67
|
+
reason: typeof reason === 'string' ? reason : null,
|
|
68
|
+
inForce: inForce === undefined ? true : read.flag(inForce, `${where}.inForce`),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** The results and the unreadable directives of a spec-guard run over briefs, `--json`. */
|
|
74
|
+
export function readGuardRun(document) {
|
|
75
|
+
const read = reader('spec-guard');
|
|
76
|
+
const top = read.object(document, 'a document');
|
|
77
|
+
const spec = (value, where) => {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const placed = read.object(value, where);
|
|
81
|
+
return { file: read.text(placed['file'], `${where}.file`), line: read.number(placed['line'], `${where}.line`) };
|
|
82
|
+
};
|
|
83
|
+
const results = read.list(top['results'], 'results').map((value, index) => {
|
|
84
|
+
const where = `results[${index}]`;
|
|
85
|
+
const result = read.object(value, where);
|
|
86
|
+
return {
|
|
87
|
+
ok: read.flag(result['ok'], `${where}.ok`),
|
|
88
|
+
description: read.text(result['description'], `${where}.description`),
|
|
89
|
+
message: read.text(result['message'], `${where}.message`),
|
|
90
|
+
spec: spec(result['spec'], `${where}.spec`),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
const errors = read.list(top['errors'], 'errors').map((value, index) => {
|
|
94
|
+
const where = `errors[${index}]`;
|
|
95
|
+
const error = read.object(value, where);
|
|
96
|
+
return {
|
|
97
|
+
message: read.text(error['message'], `${where}.message`),
|
|
98
|
+
raw: read.optionalText(error['raw'], `${where}.raw`) ?? '',
|
|
99
|
+
spec: spec(error['spec'], `${where}.spec`),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
return { results, errors };
|
|
103
|
+
}
|
|
104
|
+
const SEVERITIES = ['error', 'warning', 'note'];
|
|
105
|
+
/**
|
|
106
|
+
* What `spec-brief archive --dry-run --format json` would refuse and warn
|
|
107
|
+
* about. A document with no plan plans nothing to refuse. spec-brief gives a
|
|
108
|
+
* reason without a hint where it has no next step, and the reason is carried
|
|
109
|
+
* as it gave it: a field it left out is undefined, which JSON leaves out too.
|
|
110
|
+
*/
|
|
111
|
+
export function readArchivePlan(document) {
|
|
112
|
+
const read = reader('spec-brief archive');
|
|
113
|
+
const top = read.object(document, 'a document');
|
|
114
|
+
if (top['plan'] === undefined)
|
|
115
|
+
return { blocking: [], warnings: [] };
|
|
116
|
+
const plan = read.object(top['plan'], 'plan');
|
|
117
|
+
const reasons = (key) => read.list(plan[key], `plan.${key}`).map((value, index) => {
|
|
118
|
+
const where = `plan.${key}[${index}]`;
|
|
119
|
+
const reason = read.object(value, where);
|
|
120
|
+
const severity = read.text(reason['severity'], `${where}.severity`);
|
|
121
|
+
if (!SEVERITIES.includes(severity))
|
|
122
|
+
read.refuse(`${where}.severity`, 'error, warning or note');
|
|
123
|
+
const hint = read.optionalText(reason['hint'], `${where}.hint`);
|
|
124
|
+
const file = read.optionalText(reason['file'], `${where}.file`);
|
|
125
|
+
const line = read.optionalNumber(reason['line'], `${where}.line`);
|
|
126
|
+
const path = read.optionalText(reason['path'], `${where}.path`);
|
|
127
|
+
return {
|
|
128
|
+
rule: read.text(reason['rule'], `${where}.rule`),
|
|
129
|
+
severity: severity,
|
|
130
|
+
message: read.text(reason['message'], `${where}.message`),
|
|
131
|
+
hint,
|
|
132
|
+
file,
|
|
133
|
+
line,
|
|
134
|
+
path,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
return { blocking: reasons('blocking'), warnings: reasons('warnings') };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=answers.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/branch.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export declare function ciBranch(env: Readonly<Record<string, string | undefined
|
|
|
29
29
|
readonly branch: string;
|
|
30
30
|
readonly source: string;
|
|
31
31
|
} | null;
|
|
32
|
-
/** The first template that yields an id, in order. */
|
|
33
|
-
export declare function briefIdFromBranch(templates: readonly string[], name: string): string | null;
|
|
32
|
+
/** The first template that yields an id, in order; `null` for a head on no branch, which names no brief. */
|
|
33
|
+
export declare function briefIdFromBranch(templates: readonly string[], name: string | null): string | null;
|
|
34
34
|
/**
|
|
35
35
|
* Whether two ids name the same brief: equal as written, or equal as numbers
|
|
36
36
|
* when both are all digits, since a branch says `brief/12` of a brief whose
|
package/dist/branch.js
CHANGED
|
@@ -97,8 +97,10 @@ export function ciBranch(env) {
|
|
|
97
97
|
named('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') ??
|
|
98
98
|
named('CI_COMMIT_BRANCH'));
|
|
99
99
|
}
|
|
100
|
-
/** The first template that yields an id, in order. */
|
|
100
|
+
/** The first template that yields an id, in order; `null` for a head on no branch, which names no brief. */
|
|
101
101
|
export function briefIdFromBranch(templates, name) {
|
|
102
|
+
if (name === null)
|
|
103
|
+
return null;
|
|
102
104
|
for (const template of templates) {
|
|
103
105
|
const id = idFromBranch(template, name);
|
|
104
106
|
if (id !== null)
|
package/dist/branch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../src/branch.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../src/branch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which brief a branch is working on, read from its name.\n *\n * A round runs on a branch, and naming the branch after the brief is the one\n * convention every repository that runs agents on briefs already has. Reading\n * it from there keeps the harness stateless: no file says which brief is\n * active, so no file can say it wrongly (ADR-0004).\n *\n * A template is a branch name with `{id}` where the id goes and `*` for any\n * run of characters other than `/`: `brief/{id}`, `*\\/brief-{id}`. The id is\n * the longest run of letters and digits at that point, and must be followed\n * by the end of the name or a separator - so `brief/012-rotate-tokens` names\n * 012, and `brief/012x` names 012x, never 012.\n */\n\nconst SEPARATORS = new Set(['-', '_', '/', '.']);\n\nfunction isIdChar(ch: string): boolean {\n return /^[A-Za-z0-9]$/.test(ch);\n}\n\n/**\n * The offsets in `name` at which a template prefix can end, by dynamic\n * programming over prefix and name, so a template of many stars costs their\n * product and never an exponent.\n */\nfunction prefixEnds(prefix: string, name: string): number[] {\n // reach[j]: the prefix read so far can end at name offset j.\n let reach: boolean[] = new Array<boolean>(name.length + 1).fill(false);\n reach[0] = true;\n for (const token of prefix) {\n const next = new Array<boolean>(name.length + 1).fill(false);\n if (token === '*') {\n let open = false;\n for (let j = 0; j <= name.length; j += 1) {\n if (reach[j]) open = true;\n if (open) next[j] = true;\n if (name.charAt(j) === '/') open = false;\n }\n } else {\n for (let j = 0; j < name.length; j += 1) if (reach[j] && name.charAt(j) === token) next[j + 1] = true;\n }\n reach = next;\n }\n const ends: number[] = [];\n reach.forEach((ok, j) => {\n if (ok) ends.push(j);\n });\n return ends;\n}\n\n/** Why a template cannot be used, or `null`. */\nexport function templateError(template: string): string | null {\n const count = template.split('{id}').length - 1;\n if (count !== 1) return `\"${template}\" must contain {id} exactly once`;\n if (!template.endsWith('{id}')) return `\"${template}\" must end with {id}; whatever follows the id is the branch's own`;\n return null;\n}\n\n/** The brief id a branch name carries under one template, or `null`. */\nexport function idFromBranch(template: string, name: string): string | null {\n if (templateError(template) !== null) return null;\n const prefix = template.slice(0, -'{id}'.length);\n for (const start of prefixEnds(prefix, name)) {\n let end = start;\n while (end < name.length && isIdChar(name.charAt(end))) end += 1;\n if (end === start) continue;\n if (end < name.length && !SEPARATORS.has(name.charAt(end))) continue;\n return name.slice(start, end);\n }\n return null;\n}\n\n/**\n * The branch a CI run is building, as the forge names it, for a checkout on\n * no branch: CI checks out a commit, and without the branch a round's own\n * run would name no brief. In order: a GitHub pull request's head branch; the\n * branch a GitHub push built; a GitLab merge request's source branch; the\n * branch a GitLab pipeline built. Each is set only where it names a branch -\n * GitHub's `GITHUB_REF_NAME` names a tag too, so only when `GITHUB_REF_TYPE`\n * says `branch` - and an empty one is not set. `null` outside CI.\n */\nexport function ciBranch(env: Readonly<Record<string, string | undefined>>): { readonly branch: string; readonly source: string } | null {\n const named = (name: string): { branch: string; source: string } | null => {\n const value = env[name]?.trim() ?? '';\n return value === '' ? null : { branch: value, source: name };\n };\n return (\n named('GITHUB_HEAD_REF') ??\n (env['GITHUB_REF_TYPE'] === 'branch' ? named('GITHUB_REF_NAME') : null) ??\n named('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME') ??\n named('CI_COMMIT_BRANCH')\n );\n}\n\n/** The first template that yields an id, in order; `null` for a head on no branch, which names no brief. */\nexport function briefIdFromBranch(templates: readonly string[], name: string | null): string | null {\n if (name === null) return null;\n for (const template of templates) {\n const id = idFromBranch(template, name);\n if (id !== null) return id;\n }\n return null;\n}\n\n/**\n * Whether two ids name the same brief: equal as written, or equal as numbers\n * when both are all digits, since a branch says `brief/12` of a brief whose\n * file says `012`.\n */\nexport function sameId(a: string, b: string): boolean {\n if (a === b) return true;\n return /^\\d+$/.test(a) && /^\\d+$/.test(b) && BigInt(a) === BigInt(b);\n}\n"]}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* could not look must never exit as though it looked and found nothing
|
|
8
8
|
* (spec-core ADR-0005).
|
|
9
9
|
*/
|
|
10
|
+
import { SiblingOutputError } from './briefs.js';
|
|
10
11
|
import { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';
|
|
11
12
|
import { ConfigError, SIBLINGS } from './config.js';
|
|
12
13
|
import { describeBase, describeClaudeCode, describeClaudeRelease, describeGitHook, describePlugin, describeSignerKeys, describeSignerProblem, describeSigners, wiringState, } from './configure.js';
|
|
@@ -72,20 +73,20 @@ async function decisionsFor(workspace, options, io, given, cwd) {
|
|
|
72
73
|
const active = await activeBrief(workspace, options, io.env);
|
|
73
74
|
const { brief, note, problem } = describeActive(active);
|
|
74
75
|
if (problem !== null)
|
|
75
|
-
return {
|
|
76
|
-
|
|
77
|
-
return { decisions, problem: null };
|
|
76
|
+
return { problem };
|
|
77
|
+
return { decisions: await checkPaths(workspace, brief, note, given, cwd, reader, options.base) };
|
|
78
78
|
}
|
|
79
79
|
const SYMBOL = { allow: 'ok ', warn: 'warning', ask: 'ask ', deny: 'refused' };
|
|
80
80
|
async function guardCommand(options, io) {
|
|
81
81
|
if (options.positionals.length === 0)
|
|
82
82
|
throw new UsageError('guard needs at least one path');
|
|
83
83
|
const workspace = await openWorkspace(options, io);
|
|
84
|
-
const
|
|
85
|
-
if (problem
|
|
86
|
-
io.stderr.write(`spec-harness: ${problem}\n`);
|
|
84
|
+
const decided = await decisionsFor(workspace, options, io, options.positionals, io.cwd);
|
|
85
|
+
if ('problem' in decided) {
|
|
86
|
+
io.stderr.write(`spec-harness: ${decided.problem}\n`);
|
|
87
87
|
return EXIT_ERROR;
|
|
88
88
|
}
|
|
89
|
+
const { decisions } = decided;
|
|
89
90
|
if (options.format === 'json') {
|
|
90
91
|
io.stdout.write(json('guard', { decisions }));
|
|
91
92
|
}
|
|
@@ -102,6 +103,9 @@ async function guardCommand(options, io) {
|
|
|
102
103
|
}
|
|
103
104
|
/* -------------------------------------------------------------------- hook */
|
|
104
105
|
async function readStdin(io) {
|
|
106
|
+
// Only the built command line reads process.stdin, and the suite spawns it
|
|
107
|
+
// from dist/, outside the sweep (tests/integration/claude-code.test.ts): the
|
|
108
|
+
// lines below survive it untested in-process.
|
|
105
109
|
if (io.stdin !== undefined)
|
|
106
110
|
return io.stdin();
|
|
107
111
|
const chunks = [];
|
|
@@ -132,13 +136,13 @@ async function hookCommand(options, io) {
|
|
|
132
136
|
throw error;
|
|
133
137
|
}
|
|
134
138
|
try {
|
|
135
|
-
const
|
|
136
|
-
if (problem
|
|
139
|
+
const decided = await decisionsFor(workspace, options, io, request.paths, cwd);
|
|
140
|
+
if ('problem' in decided) {
|
|
137
141
|
// Exit 2 blocks the tool call, and its stderr reaches the model.
|
|
138
|
-
io.stderr.write(`spec-harness: ${problem}. Fix the brief or the branch before writing.\n`);
|
|
142
|
+
io.stderr.write(`spec-harness: ${decided.problem}. Fix the brief or the branch before writing.\n`);
|
|
139
143
|
return EXIT_ERROR;
|
|
140
144
|
}
|
|
141
|
-
const response = claudeResponse(request.event, decisions);
|
|
145
|
+
const response = claudeResponse(request.event, decided.decisions);
|
|
142
146
|
io.stdout.write(response.stdout);
|
|
143
147
|
return response.exitCode;
|
|
144
148
|
}
|
|
@@ -157,12 +161,12 @@ async function hookCommand(options, io) {
|
|
|
157
161
|
const paths = staged.flatMap((change) => (change.from === undefined ? [change.path] : [change.from, change.path]));
|
|
158
162
|
if (paths.length === 0)
|
|
159
163
|
return EXIT_OK;
|
|
160
|
-
const
|
|
161
|
-
if (problem
|
|
162
|
-
io.stderr.write(`spec-harness: ${problem}\n`);
|
|
164
|
+
const decided = await decisionsFor(workspace, options, io, paths, workspace.root);
|
|
165
|
+
if ('problem' in decided) {
|
|
166
|
+
io.stderr.write(`spec-harness: ${decided.problem}\n`);
|
|
163
167
|
return EXIT_ERROR;
|
|
164
168
|
}
|
|
165
|
-
const response = gitResponse(decisions);
|
|
169
|
+
const response = gitResponse(decided.decisions);
|
|
166
170
|
io.stderr.write(response.text);
|
|
167
171
|
return response.exitCode;
|
|
168
172
|
}
|
|
@@ -187,6 +191,8 @@ async function doctorCommand(options, io) {
|
|
|
187
191
|
// on the base, and only once spec-brief's archive asks the plugin about it.
|
|
188
192
|
const base = await resolveBase(workspace, options.base);
|
|
189
193
|
const signersFile = workspace.config.rulings.allowedSigners;
|
|
194
|
+
// An unresolved base has no commit: git shows nothing for one, so asking
|
|
195
|
+
// anyway reads as no file, and that mutant is equivalent.
|
|
190
196
|
const signersText = base.kind === 'resolved' ? await show(base.sha, signersFile, workspace.root) : null;
|
|
191
197
|
const onBase = base.kind === 'resolved' ? signersText !== null : null;
|
|
192
198
|
const signers = describeSigners(signersFile, base.kind === 'resolved' ? base.ref : null, onBase === true);
|
|
@@ -238,7 +244,7 @@ async function doctorCommand(options, io) {
|
|
|
238
244
|
`root ${workspace.root}`,
|
|
239
245
|
`branch ${branch}`,
|
|
240
246
|
`brief ${named ?? '(none named)'}`,
|
|
241
|
-
`base ${describeBase(base
|
|
247
|
+
`base ${describeBase(base)}`,
|
|
242
248
|
`signers ${signers}`,
|
|
243
249
|
...more(signerNotes),
|
|
244
250
|
`plugin ${describePlugin(plugin)}`,
|
|
@@ -292,7 +298,7 @@ export async function run(argv, io) {
|
|
|
292
298
|
}
|
|
293
299
|
if (options.help || options.command === undefined) {
|
|
294
300
|
io.stdout.write(HELP);
|
|
295
|
-
return options.
|
|
301
|
+
return options.help ? EXIT_OK : EXIT_ERROR;
|
|
296
302
|
}
|
|
297
303
|
const command = COMMANDS[options.command];
|
|
298
304
|
if (command === undefined) {
|
|
@@ -303,13 +309,18 @@ export async function run(argv, io) {
|
|
|
303
309
|
return await command(options, io);
|
|
304
310
|
}
|
|
305
311
|
catch (error) {
|
|
306
|
-
|
|
312
|
+
// A sibling's document of a shape the harness does not read is a sibling
|
|
313
|
+
// that printed something it cannot read (answers.ts): exit 2.
|
|
314
|
+
if (error instanceof UsageError || error instanceof ConfigError || error instanceof SiblingError || error instanceof SiblingOutputError) {
|
|
307
315
|
io.stderr.write(`spec-harness: ${error.message}\n`);
|
|
308
316
|
return EXIT_ERROR;
|
|
309
317
|
}
|
|
310
318
|
throw error;
|
|
311
319
|
}
|
|
312
320
|
}
|
|
321
|
+
// What bin/spec-harness.js runs; the suite spawns the built one, from dist/,
|
|
322
|
+
// outside the sweep (tests/integration/cli.test.ts), so it survives untested
|
|
323
|
+
// in-process.
|
|
313
324
|
export async function main(argv = process.argv.slice(2)) {
|
|
314
325
|
return run(argv, { stdout: process.stdout, stderr: process.stderr, cwd: process.cwd(), env: process.env });
|
|
315
326
|
}
|