@descent-vtt/spec-harness 0.0.0 → 0.1.1
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 +16 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +12 -0
- package/CHANGELOG.md +56 -0
- package/README.md +127 -9
- package/bin/spec-harness.js +19 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +93 -0
- package/dist/audit.js.map +1 -0
- package/dist/branch.d.ts +26 -0
- package/dist/branch.js +100 -0
- package/dist/branch.js.map +1 -0
- package/dist/briefs.d.ts +49 -0
- package/dist/briefs.js +111 -0
- package/dist/briefs.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +224 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +167 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +56 -0
- package/dist/context.js +125 -0
- package/dist/context.js.map +1 -0
- package/dist/document.d.ts +33 -0
- package/dist/document.js +9 -0
- package/dist/document.js.map +1 -0
- package/dist/fs.d.ts +29 -0
- package/dist/fs.js +85 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +57 -0
- package/dist/git.js +129 -0
- package/dist/git.js.map +1 -0
- package/dist/guard.d.ts +43 -0
- package/dist/guard.js +120 -0
- package/dist/guard.js.map +1 -0
- package/dist/hooks.d.ts +50 -0
- package/dist/hooks.js +109 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/junit.d.ts +30 -0
- package/dist/junit.js +166 -0
- package/dist/junit.js.map +1 -0
- package/dist/manifests.d.ts +52 -0
- package/dist/manifests.js +346 -0
- package/dist/manifests.js.map +1 -0
- package/dist/plugin.d.ts +45 -0
- package/dist/plugin.js +79 -0
- package/dist/plugin.js.map +1 -0
- package/dist/premises.d.ts +16 -0
- package/dist/premises.js +71 -0
- package/dist/premises.js.map +1 -0
- package/dist/probe.d.ts +108 -0
- package/dist/probe.js +203 -0
- package/dist/probe.js.map +1 -0
- package/dist/reader.d.ts +8 -0
- package/dist/reader.js +105 -0
- package/dist/reader.js.map +1 -0
- package/dist/round.d.ts +68 -0
- package/dist/round.js +339 -0
- package/dist/round.js.map +1 -0
- package/dist/rulings.d.ts +92 -0
- package/dist/rulings.js +160 -0
- package/dist/rulings.js.map +1 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +32 -0
- package/dist/setup.js +218 -0
- package/dist/setup.js.map +1 -0
- package/dist/siblings.d.ts +66 -0
- package/dist/siblings.js +134 -0
- package/dist/siblings.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/dist/versions.d.ts +42 -0
- package/dist/versions.js +57 -0
- package/dist/versions.js.map +1 -0
- package/dist/workspace.d.ts +77 -0
- package/dist/workspace.js +138 -0
- package/dist/workspace.js.map +1 -0
- package/hooks/hooks.json +28 -0
- package/package.json +85 -6
- package/skills/close-round/SKILL.md +38 -0
- package/skills/draft-brief/SKILL.md +64 -0
- package/skills/run-round/SKILL.md +46 -0
- package/skills/split-goal/SKILL.md +42 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/briefs.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The briefs, as spec-brief reports them, and which one a round is working on.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief owns the brief format and its configuration (its ADR-0003), so
|
|
5
|
+
* the harness reads briefs the way spec-brief does by asking it:
|
|
6
|
+
* `spec-brief list --archived --format json`. This module reads that
|
|
7
|
+
* document and nothing else; a shape it does not recognise is an error, never
|
|
8
|
+
* a best-effort reading of half the fields (ADR-0002).
|
|
9
|
+
*/
|
|
10
|
+
import type { BriefRow } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* The spec-brief JSON document versions this harness reads. Version 1 is the
|
|
13
|
+
* one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the
|
|
14
|
+
* harness never reads, so the `list` and `archive` documents are the same.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SUPPORTED_SCHEMA_VERSIONS: readonly number[];
|
|
17
|
+
export declare class SiblingOutputError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/** Reads the document `spec-brief list --format json` prints. */
|
|
21
|
+
export declare function parseBriefList(document: unknown): BriefRow[];
|
|
22
|
+
/** How the active brief was named, for messages. */
|
|
23
|
+
export type ActiveSource = 'flag' | 'environment' | 'branch';
|
|
24
|
+
export type ActiveBrief = {
|
|
25
|
+
readonly kind: 'found';
|
|
26
|
+
readonly brief: BriefRow;
|
|
27
|
+
readonly source: ActiveSource;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'none';
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: 'unknown';
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly source: ActiveSource;
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'archived';
|
|
37
|
+
readonly brief: BriefRow;
|
|
38
|
+
readonly source: ActiveSource;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by
|
|
42
|
+
* the branch - never guessed from which brief happens to be the only active
|
|
43
|
+
* one (ADR-0004).
|
|
44
|
+
*/
|
|
45
|
+
export declare function findActive(briefs: readonly BriefRow[], named: {
|
|
46
|
+
readonly flag?: string | undefined;
|
|
47
|
+
readonly environment?: string | undefined;
|
|
48
|
+
readonly branch?: string | null | undefined;
|
|
49
|
+
}): ActiveBrief;
|
package/dist/briefs.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The briefs, as spec-brief reports them, and which one a round is working on.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief owns the brief format and its configuration (its ADR-0003), so
|
|
5
|
+
* the harness reads briefs the way spec-brief does by asking it:
|
|
6
|
+
* `spec-brief list --archived --format json`. This module reads that
|
|
7
|
+
* document and nothing else; a shape it does not recognise is an error, never
|
|
8
|
+
* a best-effort reading of half the fields (ADR-0002).
|
|
9
|
+
*/
|
|
10
|
+
import { sameId } from './branch.js';
|
|
11
|
+
/**
|
|
12
|
+
* The spec-brief JSON document versions this harness reads. Version 1 is the
|
|
13
|
+
* one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the
|
|
14
|
+
* harness never reads, so the `list` and `archive` documents are the same.
|
|
15
|
+
*/
|
|
16
|
+
export const SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3];
|
|
17
|
+
export class SiblingOutputError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'SiblingOutputError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isObject(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function strings(value, where) {
|
|
27
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
28
|
+
throw new SiblingOutputError(`${where} is not a list of strings`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function nullableString(value, where) {
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof value !== 'string')
|
|
36
|
+
throw new SiblingOutputError(`${where} is not a string`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** Reads the document `spec-brief list --format json` prints. */
|
|
40
|
+
export function parseBriefList(document) {
|
|
41
|
+
if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {
|
|
42
|
+
throw new SiblingOutputError('spec-brief did not print a list document');
|
|
43
|
+
}
|
|
44
|
+
const version = document['schemaVersion'];
|
|
45
|
+
if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {
|
|
46
|
+
throw new SiblingOutputError(`spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
const briefs = document['briefs'];
|
|
49
|
+
if (!Array.isArray(briefs))
|
|
50
|
+
throw new SiblingOutputError('spec-brief list has no "briefs"');
|
|
51
|
+
return briefs.map((row, index) => {
|
|
52
|
+
const where = `briefs[${index}]`;
|
|
53
|
+
if (!isObject(row))
|
|
54
|
+
throw new SiblingOutputError(`${where} is not an object`);
|
|
55
|
+
const id = row['id'];
|
|
56
|
+
const file = row['file'];
|
|
57
|
+
if (typeof id !== 'string' || typeof file !== 'string')
|
|
58
|
+
throw new SiblingOutputError(`${where} has no id or file`);
|
|
59
|
+
const phase = row['phase'];
|
|
60
|
+
if (phase !== 'live' && phase !== 'archived')
|
|
61
|
+
throw new SiblingOutputError(`${where}.phase is "${String(phase)}"`);
|
|
62
|
+
const tasks = row['tasks'];
|
|
63
|
+
const wave = row['wave'];
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
file,
|
|
67
|
+
title: nullableString(row['title'], `${where}.title`),
|
|
68
|
+
phase,
|
|
69
|
+
status: nullableString(row['status'], `${where}.status`),
|
|
70
|
+
type: nullableString(row['type'], `${where}.type`),
|
|
71
|
+
wave: typeof wave === 'number' ? wave : null,
|
|
72
|
+
dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),
|
|
73
|
+
affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),
|
|
74
|
+
protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),
|
|
75
|
+
tasks: {
|
|
76
|
+
total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,
|
|
77
|
+
checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,
|
|
78
|
+
},
|
|
79
|
+
ready: row['ready'] === true,
|
|
80
|
+
waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by
|
|
86
|
+
* the branch - never guessed from which brief happens to be the only active
|
|
87
|
+
* one (ADR-0004).
|
|
88
|
+
*/
|
|
89
|
+
export function findActive(briefs, named) {
|
|
90
|
+
const pick = (id, source) => {
|
|
91
|
+
const matches = briefs.filter((brief) => sameId(brief.id, id));
|
|
92
|
+
const live = matches.find((brief) => brief.phase === 'live');
|
|
93
|
+
if (live !== undefined)
|
|
94
|
+
return { kind: 'found', brief: live, source };
|
|
95
|
+
const archived = matches[0];
|
|
96
|
+
if (archived !== undefined)
|
|
97
|
+
return { kind: 'archived', brief: archived, source };
|
|
98
|
+
return { kind: 'unknown', id, source };
|
|
99
|
+
};
|
|
100
|
+
if (named.flag !== undefined && named.flag.trim() !== '')
|
|
101
|
+
return pick(named.flag.trim(), 'flag');
|
|
102
|
+
if (named.environment !== undefined && named.environment.trim() !== '')
|
|
103
|
+
return pick(named.environment.trim(), 'environment');
|
|
104
|
+
if (named.branch !== undefined && named.branch !== null)
|
|
105
|
+
return pick(named.branch, 'branch');
|
|
106
|
+
return {
|
|
107
|
+
kind: 'none',
|
|
108
|
+
reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=briefs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"briefs.js","sourceRoot":"","sources":["../src/briefs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The briefs, as spec-brief reports them, and which one a round is working on.\n *\n * spec-brief owns the brief format and its configuration (its ADR-0003), so\n * the harness reads briefs the way spec-brief does by asking it:\n * `spec-brief list --archived --format json`. This module reads that\n * document and nothing else; a shape it does not recognise is an error, never\n * a best-effort reading of half the fields (ADR-0002).\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\n/**\n * The spec-brief JSON document versions this harness reads. Version 1 is the\n * one spec-brief 0.1.0 prints; version 2 changed only `matrix`, which the\n * harness never reads, so the `list` and `archive` documents are the same.\n */\nexport const SUPPORTED_SCHEMA_VERSIONS: readonly number[] = [1, 2, 3];\n\nexport class SiblingOutputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingOutputError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction strings(value: unknown, where: string): string[] {\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {\n throw new SiblingOutputError(`${where} is not a list of strings`);\n }\n return value;\n}\n\nfunction nullableString(value: unknown, where: string): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') throw new SiblingOutputError(`${where} is not a string`);\n return value;\n}\n\n/** Reads the document `spec-brief list --format json` prints. */\nexport function parseBriefList(document: unknown): BriefRow[] {\n if (!isObject(document) || document['tool'] !== 'spec-brief' || document['command'] !== 'list') {\n throw new SiblingOutputError('spec-brief did not print a list document');\n }\n const version = document['schemaVersion'];\n if (typeof version !== 'number' || !SUPPORTED_SCHEMA_VERSIONS.includes(version)) {\n throw new SiblingOutputError(\n `spec-brief printed schemaVersion ${String(version)}; this harness reads ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}`,\n );\n }\n const briefs = document['briefs'];\n if (!Array.isArray(briefs)) throw new SiblingOutputError('spec-brief list has no \"briefs\"');\n return briefs.map((row, index) => {\n const where = `briefs[${index}]`;\n if (!isObject(row)) throw new SiblingOutputError(`${where} is not an object`);\n const id = row['id'];\n const file = row['file'];\n if (typeof id !== 'string' || typeof file !== 'string') throw new SiblingOutputError(`${where} has no id or file`);\n const phase = row['phase'];\n if (phase !== 'live' && phase !== 'archived') throw new SiblingOutputError(`${where}.phase is \"${String(phase)}\"`);\n const tasks = row['tasks'];\n const wave = row['wave'];\n return {\n id,\n file,\n title: nullableString(row['title'], `${where}.title`),\n phase,\n status: nullableString(row['status'], `${where}.status`),\n type: nullableString(row['type'], `${where}.type`),\n wave: typeof wave === 'number' ? wave : null,\n dependsOn: strings(row['dependsOn'] ?? [], `${where}.dependsOn`),\n affectedFiles: strings(row['affectedFiles'] ?? [], `${where}.affectedFiles`),\n protectedFiles: strings(row['protectedFiles'] ?? [], `${where}.protectedFiles`),\n tasks: {\n total: isObject(tasks) && typeof tasks['total'] === 'number' ? tasks['total'] : 0,\n checked: isObject(tasks) && typeof tasks['checked'] === 'number' ? tasks['checked'] : 0,\n },\n ready: row['ready'] === true,\n waitingOn: strings(row['waitingOn'] ?? [], `${where}.waitingOn`),\n };\n });\n}\n\n/** How the active brief was named, for messages. */\nexport type ActiveSource = 'flag' | 'environment' | 'branch';\n\nexport type ActiveBrief =\n | { readonly kind: 'found'; readonly brief: BriefRow; readonly source: ActiveSource }\n | { readonly kind: 'none'; readonly reason: string }\n | { readonly kind: 'unknown'; readonly id: string; readonly source: ActiveSource }\n | { readonly kind: 'archived'; readonly brief: BriefRow; readonly source: ActiveSource };\n\n/**\n * The brief a round is working on: named by a flag, by `SPEC_BRIEF`, or by\n * the branch - never guessed from which brief happens to be the only active\n * one (ADR-0004).\n */\nexport function findActive(\n briefs: readonly BriefRow[],\n named: { readonly flag?: string | undefined; readonly environment?: string | undefined; readonly branch?: string | null | undefined },\n): ActiveBrief {\n const pick = (id: string, source: ActiveSource): ActiveBrief => {\n const matches = briefs.filter((brief) => sameId(brief.id, id));\n const live = matches.find((brief) => brief.phase === 'live');\n if (live !== undefined) return { kind: 'found', brief: live, source };\n const archived = matches[0];\n if (archived !== undefined) return { kind: 'archived', brief: archived, source };\n return { kind: 'unknown', id, source };\n };\n if (named.flag !== undefined && named.flag.trim() !== '') return pick(named.flag.trim(), 'flag');\n if (named.environment !== undefined && named.environment.trim() !== '') return pick(named.environment.trim(), 'environment');\n if (named.branch !== undefined && named.branch !== null) return pick(named.branch, 'branch');\n return {\n kind: 'none',\n reason: 'no brief is named: pass --brief <id>, set SPEC_BRIEF, or work on a branch named like brief/<id>-<topic>',\n };\n}\n"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line: arguments, dispatch, output and exit codes.
|
|
3
|
+
*
|
|
4
|
+
* Exit 0: done, nothing refused. Exit 1: something refused or found. Exit 2:
|
|
5
|
+
* the answer cannot be trusted - a bad flag, a configuration that does not
|
|
6
|
+
* load, a sibling that is missing or printed something unreadable. A run that
|
|
7
|
+
* could not look must never exit as though it looked and found nothing
|
|
8
|
+
* (spec-core ADR-0005).
|
|
9
|
+
*/
|
|
10
|
+
import { type CliIO } from './workspace.js';
|
|
11
|
+
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version, type CliIO } from './workspace.js';
|
|
12
|
+
export declare const HELP = "spec-harness - context, guards, audits and rulings for rounds of agent work\n\nUsage:\n spec-harness <command> [options]\n\nCommands:\n context [brief] What an agent needs to start a round: the brief, its scope,\n rulings, dependencies, the rules in force, the cited documents.\n guard <path...> May the round write these paths? Exit 1 if one is refused.\n hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).\n hook git Answer git's pre-commit hook for the staged files.\n escalate Ask a person to rule: --path <file> (repeatable) --reason <why>\n [--option \"<label>: <cost>\"]... [--recommend <text>].\n --list shows what waits; --show <id> prints one memo.\n rule <id> Record a person's ruling in the brief: --allow or --deny, with\n --note. 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";
|
|
13
|
+
export declare function run(argv: readonly string[], io: CliIO): Promise<number>;
|
|
14
|
+
export declare function main(argv?: readonly string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line: arguments, dispatch, output and exit codes.
|
|
3
|
+
*
|
|
4
|
+
* Exit 0: done, nothing refused. Exit 1: something refused or found. Exit 2:
|
|
5
|
+
* the answer cannot be trusted - a bad flag, a configuration that does not
|
|
6
|
+
* load, a sibling that is missing or printed something unreadable. A run that
|
|
7
|
+
* could not look must never exit as though it looked and found nothing
|
|
8
|
+
* (spec-core ADR-0005).
|
|
9
|
+
*/
|
|
10
|
+
import { auditCommand, contextCommand, escalateCommand, probeCommand, ruleCommand, rulingsCommand } from './commands.js';
|
|
11
|
+
import { ConfigError, SIBLINGS } from './config.js';
|
|
12
|
+
import { stagedChanges } from './git.js';
|
|
13
|
+
import { claudeResponse, gitResponse, parseClaudeHook } from './hooks.js';
|
|
14
|
+
import { premisesCommand } from './premises.js';
|
|
15
|
+
import { createReader } from './reader.js';
|
|
16
|
+
import { checkPaths } from './round.js';
|
|
17
|
+
import { mcpCommand } from './server.js';
|
|
18
|
+
import { initCommand } from './setup.js';
|
|
19
|
+
import { SiblingError } from './siblings.js';
|
|
20
|
+
import { MINIMUM_VERSIONS } from './versions.js';
|
|
21
|
+
import { activeBrief, describeActive, EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, namedId, openWorkspace, parseOptions, UsageError, version, } from './workspace.js';
|
|
22
|
+
export { EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './workspace.js';
|
|
23
|
+
export const HELP = `spec-harness - context, guards, audits and rulings for rounds of agent work
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
spec-harness <command> [options]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
context [brief] What an agent needs to start a round: the brief, its scope,
|
|
30
|
+
rulings, dependencies, the rules in force, the cited documents.
|
|
31
|
+
guard <path...> May the round write these paths? Exit 1 if one is refused.
|
|
32
|
+
hook claude Answer a Claude Code PreToolUse or PostToolUse hook (stdin).
|
|
33
|
+
hook git Answer git's pre-commit hook for the staged files.
|
|
34
|
+
escalate Ask a person to rule: --path <file> (repeatable) --reason <why>
|
|
35
|
+
[--option "<label>: <cost>"]... [--recommend <text>].
|
|
36
|
+
--list shows what waits; --show <id> prints one memo.
|
|
37
|
+
rule <id> Record a person's ruling in the brief: --allow or --deny, with
|
|
38
|
+
--note. It counts once committed signed.
|
|
39
|
+
rulings [brief] The brief's rulings, and whether each one's signature verifies.
|
|
40
|
+
audit [brief] Did the round stay inside the lines: what the archive would say,
|
|
41
|
+
the brief's assertions, unverified rulings, new dependencies.
|
|
42
|
+
probe [brief] Run the brief's probes: red at the base, green at the head.
|
|
43
|
+
--at base|head|both, --id <probe>.
|
|
44
|
+
premises Do the live briefs' premises still hold? For CI: exit 1 when one is stale.
|
|
45
|
+
init Configure the spec-* tools to agree. Prints the plan; --write
|
|
46
|
+
applies it; --git-hook adds a pre-commit hook.
|
|
47
|
+
mcp Serve start_round, check_path, request_escalation, audit_round,
|
|
48
|
+
list_rounds and the workflow prompts over MCP on stdio.
|
|
49
|
+
doctor Which siblings are installed, at which versions, and which
|
|
50
|
+
brief is named. Exit 1 when one is older than this release needs.
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--brief <id> The brief the round works on. Otherwise SPEC_BRIEF, then the branch.
|
|
54
|
+
--base <ref> What the round is measured from. Otherwise "base" in
|
|
55
|
+
.spec-harness.json, then the remote's default branch.
|
|
56
|
+
--root <dir> Run from another directory.
|
|
57
|
+
--format <fmt> pretty or json.
|
|
58
|
+
--strict Warnings fail the run.
|
|
59
|
+
--help, --version
|
|
60
|
+
|
|
61
|
+
Exit codes: 0 clean, 1 refused or found something, 2 the answer cannot be trusted.
|
|
62
|
+
`;
|
|
63
|
+
/* ------------------------------------------------------------------- guard */
|
|
64
|
+
const reader = createReader();
|
|
65
|
+
async function decisionsFor(workspace, options, io, given, cwd) {
|
|
66
|
+
const active = await activeBrief(workspace, options, io.env);
|
|
67
|
+
const { brief, note, problem } = describeActive(active);
|
|
68
|
+
if (problem !== null)
|
|
69
|
+
return { decisions: [], problem };
|
|
70
|
+
const decisions = await checkPaths(workspace, brief, note, given, cwd, reader);
|
|
71
|
+
return { decisions, problem: null };
|
|
72
|
+
}
|
|
73
|
+
const SYMBOL = { allow: 'ok ', warn: 'warning', ask: 'ask ', deny: 'refused' };
|
|
74
|
+
async function guardCommand(options, io) {
|
|
75
|
+
if (options.positionals.length === 0)
|
|
76
|
+
throw new UsageError('guard needs at least one path');
|
|
77
|
+
const workspace = await openWorkspace(options, io);
|
|
78
|
+
const { decisions, problem } = await decisionsFor(workspace, options, io, options.positionals, io.cwd);
|
|
79
|
+
if (problem !== null) {
|
|
80
|
+
io.stderr.write(`spec-harness: ${problem}\n`);
|
|
81
|
+
return EXIT_ERROR;
|
|
82
|
+
}
|
|
83
|
+
if (options.format === 'json') {
|
|
84
|
+
io.stdout.write(json('guard', { decisions }));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const decision of decisions) {
|
|
88
|
+
io.stdout.write(`${SYMBOL[decision.verdict]} ${decision.message}\n`);
|
|
89
|
+
if (decision.verdict !== 'allow')
|
|
90
|
+
io.stdout.write(` ${decision.hint}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const refused = decisions.some((d) => d.verdict === 'deny' || d.verdict === 'ask');
|
|
94
|
+
const warned = decisions.some((d) => d.verdict === 'warn');
|
|
95
|
+
return refused || (options.strict && warned) ? EXIT_FAILED : EXIT_OK;
|
|
96
|
+
}
|
|
97
|
+
/* -------------------------------------------------------------------- hook */
|
|
98
|
+
async function readStdin(io) {
|
|
99
|
+
if (io.stdin !== undefined)
|
|
100
|
+
return io.stdin();
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for await (const chunk of process.stdin)
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
105
|
+
}
|
|
106
|
+
async function hookCommand(options, io) {
|
|
107
|
+
const kind = options.positionals[0];
|
|
108
|
+
if (kind === 'claude') {
|
|
109
|
+
const request = parseClaudeHook(await readStdin(io));
|
|
110
|
+
if ('error' in request) {
|
|
111
|
+
// A hook that cannot read its question refuses nothing it cannot see,
|
|
112
|
+
// but says so where the person reads it: a non-blocking error.
|
|
113
|
+
io.stderr.write(`spec-harness: ${request.error}\n`);
|
|
114
|
+
return EXIT_FAILED;
|
|
115
|
+
}
|
|
116
|
+
if (request.paths.length === 0)
|
|
117
|
+
return EXIT_OK;
|
|
118
|
+
const cwd = request.cwd ?? io.cwd;
|
|
119
|
+
let workspace;
|
|
120
|
+
try {
|
|
121
|
+
workspace = await openWorkspace({ ...options, root: options.root ?? io.env['CLAUDE_PROJECT_DIR'] ?? cwd }, io);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof UsageError)
|
|
125
|
+
return EXIT_OK;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const { decisions, problem } = await decisionsFor(workspace, options, io, request.paths, cwd);
|
|
130
|
+
if (problem !== null) {
|
|
131
|
+
// Exit 2 blocks the tool call, and its stderr reaches the model.
|
|
132
|
+
io.stderr.write(`spec-harness: ${problem}. Fix the brief or the branch before writing.\n`);
|
|
133
|
+
return EXIT_ERROR;
|
|
134
|
+
}
|
|
135
|
+
const response = claudeResponse(request.event, decisions);
|
|
136
|
+
io.stdout.write(response.stdout);
|
|
137
|
+
return response.exitCode;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (request.event !== 'PreToolUse')
|
|
141
|
+
throw error;
|
|
142
|
+
// A round is named and its briefs cannot be read: the guard cannot
|
|
143
|
+
// tell a protected file from any other, so the write waits.
|
|
144
|
+
io.stderr.write(`spec-harness: cannot check this write: ${error.message}\n`);
|
|
145
|
+
return EXIT_ERROR;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (kind === 'git') {
|
|
149
|
+
const workspace = await openWorkspace(options, io);
|
|
150
|
+
const staged = await stagedChanges(workspace.root);
|
|
151
|
+
const paths = staged.flatMap((change) => (change.from === undefined ? [change.path] : [change.from, change.path]));
|
|
152
|
+
if (paths.length === 0)
|
|
153
|
+
return EXIT_OK;
|
|
154
|
+
const { decisions, problem } = await decisionsFor(workspace, options, io, paths, workspace.root);
|
|
155
|
+
if (problem !== null) {
|
|
156
|
+
io.stderr.write(`spec-harness: ${problem}\n`);
|
|
157
|
+
return EXIT_ERROR;
|
|
158
|
+
}
|
|
159
|
+
const response = gitResponse(decisions);
|
|
160
|
+
io.stderr.write(response.text);
|
|
161
|
+
return response.exitCode;
|
|
162
|
+
}
|
|
163
|
+
throw new UsageError('hook takes "claude" or "git"');
|
|
164
|
+
}
|
|
165
|
+
/* ------------------------------------------------------------------ doctor */
|
|
166
|
+
async function doctorCommand(options, io) {
|
|
167
|
+
const workspace = await openWorkspace(options, io);
|
|
168
|
+
const rows = SIBLINGS.map((name) => {
|
|
169
|
+
const sibling = workspace.siblings.locate(name);
|
|
170
|
+
const row = { tool: name, state: sibling.kind, minimum: MINIMUM_VERSIONS[name] };
|
|
171
|
+
if (sibling.kind === 'absent')
|
|
172
|
+
return { ...row, version: null, detail: sibling.reason };
|
|
173
|
+
if (sibling.kind === 'outdated')
|
|
174
|
+
return { ...row, version: sibling.version, detail: sibling.reason };
|
|
175
|
+
const command = sibling.command.join(' ');
|
|
176
|
+
const detail = sibling.version === null ? `${command} (named in .spec-harness.json; its version is not checked)` : `${command} (${sibling.version})`;
|
|
177
|
+
return { ...row, version: sibling.version, detail };
|
|
178
|
+
});
|
|
179
|
+
const named = namedId(workspace, options, io.env);
|
|
180
|
+
if (options.format === 'json') {
|
|
181
|
+
io.stdout.write(json('doctor', { root: workspace.root, branch: workspace.branch, brief: named, siblings: rows }));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
io.stdout.write(`root ${workspace.root}\nbranch ${workspace.branch ?? '(detached)'}\nbrief ${named ?? '(none named)'}\n\n`);
|
|
185
|
+
for (const row of rows)
|
|
186
|
+
io.stdout.write(`${row.state.padEnd(8)} ${row.tool.padEnd(10)} ${row.detail}\n`);
|
|
187
|
+
}
|
|
188
|
+
// An outdated sibling is a problem to fix, where a missing optional one is
|
|
189
|
+
// a choice; with none installed, the harness can check nothing.
|
|
190
|
+
const outdated = rows.some((row) => row.state === 'outdated');
|
|
191
|
+
const usable = rows.some((row) => row.state === 'found');
|
|
192
|
+
return outdated || !usable ? EXIT_FAILED : EXIT_OK;
|
|
193
|
+
}
|
|
194
|
+
/* ---------------------------------------------------------------- dispatch */
|
|
195
|
+
const COMMANDS = {
|
|
196
|
+
guard: guardCommand,
|
|
197
|
+
hook: hookCommand,
|
|
198
|
+
context: contextCommand,
|
|
199
|
+
audit: auditCommand,
|
|
200
|
+
escalate: escalateCommand,
|
|
201
|
+
rule: ruleCommand,
|
|
202
|
+
rulings: rulingsCommand,
|
|
203
|
+
probe: probeCommand,
|
|
204
|
+
premises: premisesCommand,
|
|
205
|
+
init: initCommand,
|
|
206
|
+
mcp: mcpCommand,
|
|
207
|
+
doctor: doctorCommand,
|
|
208
|
+
};
|
|
209
|
+
export async function run(argv, io) {
|
|
210
|
+
let options;
|
|
211
|
+
try {
|
|
212
|
+
options = parseOptions(argv);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
io.stderr.write(`spec-harness: ${error.message}\n`);
|
|
216
|
+
return EXIT_ERROR;
|
|
217
|
+
}
|
|
218
|
+
if (options.version) {
|
|
219
|
+
io.stdout.write(`${version()}\n`);
|
|
220
|
+
return EXIT_OK;
|
|
221
|
+
}
|
|
222
|
+
if (options.help || options.command === undefined) {
|
|
223
|
+
io.stdout.write(HELP);
|
|
224
|
+
return options.command === undefined && !options.help ? EXIT_ERROR : EXIT_OK;
|
|
225
|
+
}
|
|
226
|
+
const command = COMMANDS[options.command];
|
|
227
|
+
if (command === undefined) {
|
|
228
|
+
io.stderr.write(`spec-harness: unknown command "${options.command}"; see spec-harness --help\n`);
|
|
229
|
+
return EXIT_ERROR;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return await command(options, io);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof UsageError || error instanceof ConfigError || error instanceof SiblingError) {
|
|
236
|
+
io.stderr.write(`spec-harness: ${error.message}\n`);
|
|
237
|
+
return EXIT_ERROR;
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
243
|
+
return run(argv, { stdout: process.stdout, stderr: process.stderr, cwd: process.cwd(), env: process.env });
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commands that read a round: context, audit, escalate, rule, rulings
|
|
3
|
+
* and probe. Each resolves the brief, asks `round.ts` for the facts and
|
|
4
|
+
* prints them; exit codes follow the family contract.
|
|
5
|
+
*/
|
|
6
|
+
import type { BriefRow } from './types.js';
|
|
7
|
+
import { type CliIO, type Options, type Workspace } from './workspace.js';
|
|
8
|
+
/** The brief a command acts on: a positional id, else the active one. A command that needs one and has none is a usage error. */
|
|
9
|
+
export declare function targetBrief(workspace: Workspace, options: Options, io: CliIO, positional: string | undefined): Promise<{
|
|
10
|
+
brief: BriefRow;
|
|
11
|
+
briefs: BriefRow[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function contextCommand(options: Options, io: CliIO): Promise<number>;
|
|
14
|
+
export declare function auditCommand(options: Options, io: CliIO): Promise<number>;
|
|
15
|
+
export declare function escalateCommand(options: Options, io: CliIO): Promise<number>;
|
|
16
|
+
export declare function ruleCommand(options: Options, io: CliIO): Promise<number>;
|
|
17
|
+
export declare function rulingsCommand(options: Options, io: CliIO): Promise<number>;
|
|
18
|
+
export declare function probeCommand(options: Options, io: CliIO): Promise<number>;
|