@descent-vtt/spec-harness 0.18.0 → 0.20.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.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +372 -0
  4. package/README.md +78 -11
  5. package/dist/briefs.d.ts +45 -0
  6. package/dist/briefs.js +73 -0
  7. package/dist/briefs.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +384 -84
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +7 -2
  12. package/dist/commands.js.map +1 -1
  13. package/dist/config.d.ts +15 -1
  14. package/dist/config.js +16 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/configure.d.ts +142 -32
  17. package/dist/configure.js +316 -89
  18. package/dist/configure.js.map +1 -1
  19. package/dist/fs.d.ts +150 -3
  20. package/dist/fs.js +459 -17
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +85 -9
  23. package/dist/git.js +140 -16
  24. package/dist/git.js.map +1 -1
  25. package/dist/hooks.d.ts +111 -0
  26. package/dist/hooks.js +172 -16
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/round.d.ts +8 -0
  29. package/dist/round.js +32 -9
  30. package/dist/round.js.map +1 -1
  31. package/dist/sandbox.js +4 -3
  32. package/dist/sandbox.js.map +1 -1
  33. package/dist/server.d.ts +25 -1
  34. package/dist/server.js +194 -27
  35. package/dist/server.js.map +1 -1
  36. package/dist/setup.d.ts +6 -3
  37. package/dist/setup.js +74 -23
  38. package/dist/setup.js.map +1 -1
  39. package/dist/siblings.d.ts +42 -7
  40. package/dist/siblings.js +135 -30
  41. package/dist/siblings.js.map +1 -1
  42. package/dist/types.d.ts +5 -0
  43. package/dist/types.js.map +1 -1
  44. package/dist/usage.d.ts +66 -12
  45. package/dist/usage.js +134 -21
  46. package/dist/usage.js.map +1 -1
  47. package/dist/workspace.d.ts +80 -1
  48. package/dist/workspace.js +188 -8
  49. package/dist/workspace.js.map +1 -1
  50. package/hooks/hooks.json +16 -0
  51. package/package.json +2 -2
package/dist/siblings.js CHANGED
@@ -15,30 +15,48 @@
15
15
  import { execFile } from 'node:child_process';
16
16
  import { existsSync, readFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
- import { parseBriefList, SiblingOutputError } from './briefs.js';
18
+ import { inputsOf, parseBriefList, SiblingOutputError } from './briefs.js';
19
+ import { hasher, isFile, keepAnswer, keptAnswer, readsAsListed } from './fs.js';
20
+ import { ANSWER_MS, stoppedAtLimit, WORK_MS } from './git.js';
19
21
  import { checkVersion } from './versions.js';
20
- /** Where a sibling is, or why it cannot be run. */
21
- export function locate(name, root, config) {
22
+ /**
23
+ * Where a sibling is, or why it cannot be run. It is looked for in `root`'s
24
+ * install, then in each of `installs` in turn, and the first that has it
25
+ * decides, at this release's floor: a work tree with an install of its own
26
+ * runs it, outdated or not, and one with none is checked with the
27
+ * project's, as git's hook and Claude Code's are where a linked worktree has
28
+ * none (ADR-0005).
29
+ */
30
+ // A name a mutant puts on the default list is a directory relative to where
31
+ // the run is, which holds no install: equivalent, here and below.
32
+ export function locate(name, root, config, installs = []) {
22
33
  // A command the configuration names is run as named: which package it runs,
23
34
  // and so which version, is the configuration's to say.
24
35
  const configured = config.tools[name];
25
36
  if (configured !== null)
26
37
  return { kind: 'found', command: configured, version: null };
27
- const directory = join(root, 'node_modules', '@descent-vtt', name);
28
- const bin = join(directory, 'bin', `${name}.js`);
29
- if (!existsSync(bin)) {
30
- return {
31
- kind: 'absent',
32
- reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under "tools" in .spec-harness.json`,
33
- };
38
+ for (const place of [root, ...installs]) {
39
+ const directory = join(place, 'node_modules', '@descent-vtt', name);
40
+ const bin = join(directory, 'bin', `${name}.js`);
41
+ if (!existsSync(bin))
42
+ continue;
43
+ const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));
44
+ const where = place === root ? {} : { elsewhere: place };
45
+ if (!check.ok)
46
+ return { kind: 'outdated', reason: check.reason, version: check.version, ...where };
47
+ return { kind: 'found', command: [process.execPath, bin], version: check.version, ...where };
34
48
  }
35
- const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));
36
- if (!check.ok)
37
- return { kind: 'outdated', reason: check.reason, version: check.version };
38
- return { kind: 'found', command: [process.execPath, bin], version: check.version };
49
+ return {
50
+ kind: 'absent',
51
+ reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under "tools" in .spec-harness.json`,
52
+ };
39
53
  }
40
54
  /** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */
41
55
  export function declaredVersion(file) {
56
+ // Only a file is read: a named pipe under the name was read without end,
57
+ // and before any sibling ran (ADR-0002).
58
+ if (!isFile(file))
59
+ return undefined;
42
60
  // A manifest that holds null throws on the read, and any other value that
43
61
  // is not an object has no version, so the catch answers for both. Falling
44
62
  // out of it answers undefined as well, and a Buffer parses as its text:
@@ -59,11 +77,15 @@ export function declaredVersion(file) {
59
77
  * `["node", "path/to/bin.js"]` works on every host; a Windows `.cmd` shim such
60
78
  * as `npx` cannot be started without a shell, and the error says to name the
61
79
  * script instead.
80
+ *
81
+ * A sibling that does not answer within `limit` is stopped, and rejected as
82
+ * a `SiblingError` that names it: what it was asked waits on it, the hook
83
+ * before a write among them, which then holds the write (ADR-0002).
62
84
  */
63
- export function runSibling(command, args, root, timeoutMs = 600_000) {
85
+ export function runSibling(name, command, args, root, limit = WORK_MS) {
64
86
  const [first, ...rest] = command;
65
87
  const program = first === 'node' ? process.execPath : first;
66
- return new Promise((resolve) => {
88
+ return new Promise((resolve, reject) => {
67
89
  execFile(program, [...rest, ...args], {
68
90
  cwd: root,
69
91
  maxBuffer: 256 * 1024 * 1024,
@@ -71,10 +93,14 @@ export function runSibling(command, args, root, timeoutMs = 600_000) {
71
93
  // Keeps a console window from opening on Windows; the sweep runs on
72
94
  // Linux, where the mutant is equivalent.
73
95
  windowsHide: true,
74
- timeout: timeoutMs,
96
+ timeout: limit,
75
97
  shell: false,
76
98
  env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
77
99
  }, (error, stdout, stderr) => {
100
+ if (stoppedAtLimit(error)) {
101
+ reject(new SiblingError(`${name} did not answer within ${limit / 1000} seconds, and was stopped`));
102
+ return;
103
+ }
78
104
  const failure = error;
79
105
  if (failure !== null && failure.code === 'ENOENT') {
80
106
  resolve({
@@ -89,6 +115,45 @@ export function runSibling(command, args, root, timeoutMs = 600_000) {
89
115
  });
90
116
  });
91
117
  }
118
+ /**
119
+ * Where and under what a session's answer of spec-brief's is kept, or
120
+ * `null` where none is. A command `tools` names is started every time:
121
+ * which version it runs cannot be told without starting it.
122
+ */
123
+ async function keeperOf(kept, root, sibling) {
124
+ // A command with no version is kept nothing by the test of the version an
125
+ // answer names, too: the mutant without this one reads, for every write, a
126
+ // kept answer there is none of, and is equivalent.
127
+ if (sibling.version === null)
128
+ return null;
129
+ const hash = await hasher();
130
+ const key = answerKey(root, kept.session, sibling.command, sibling.version);
131
+ return { commonDir: kept.commonDir, key, name: `${hash(key)}.json`, version: sibling.version, hash };
132
+ }
133
+ /**
134
+ * The briefs a kept answer gives, or `null` where it is not to be given:
135
+ * nothing kept, or what was kept is not a list document that names what it
136
+ * was read from in the version installed, or anything it names reads
137
+ * otherwise.
138
+ */
139
+ async function givenAgain(stdout, version, root, hash) {
140
+ // JSON.parse reads null as null, which names no inputs, and a null read
141
+ // past the test of none below throws, which answers null: the two tests
142
+ // are the compiler's, and their mutants equivalent.
143
+ if (stdout === null)
144
+ return null;
145
+ try {
146
+ const document = JSON.parse(stdout);
147
+ const inputs = inputsOf(document);
148
+ if (inputs === null || inputs.tool !== version || !(await readsAsListed(root, inputs, hash)))
149
+ return null;
150
+ return parseBriefList(document);
151
+ }
152
+ catch {
153
+ // What another program wrote there is no answer of spec-brief's.
154
+ return null;
155
+ }
156
+ }
92
157
  export class SiblingError extends Error {
93
158
  constructor(message) {
94
159
  super(message);
@@ -104,23 +169,56 @@ function parseJson(name, run) {
104
169
  throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);
105
170
  }
106
171
  }
107
- export function createSiblings(root, config) {
108
- const found = (name) => {
109
- const sibling = locate(name, root, config);
110
- if (sibling.kind !== 'found')
111
- throw new SiblingError(sibling.reason);
112
- return sibling.command;
113
- };
172
+ /** The command line spec-brief is asked for the briefs with. */
173
+ const LIST = ['list', '--archived', '--format', 'json', '--no-color'];
174
+ /**
175
+ * The variables beyond git's own that an answer of spec-brief's can turn on
176
+ * and its list does not name: which git and which Node run, where git finds
177
+ * the person's configuration and ignore files, and how Node runs a script.
178
+ * Every variable whose name begins `GIT_` is read with them.
179
+ */
180
+ const ANSWER_VARIABLES = ['PATH', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'XDG_CONFIG_HOME', 'NODE_OPTIONS', 'NODE_PATH'];
181
+ /**
182
+ * What a kept answer is the answer to: the work tree, the session, the
183
+ * spec-brief that answered, by its command, its version and the Node it ran
184
+ * on, the command line, and the variables above, as spec-brief is started
185
+ * with them (runSibling). An answer is given again only for the same.
186
+ */
187
+ export function answerKey(root, session, command, version, env = process.env) {
188
+ const variables = Object.keys(env)
189
+ .filter((name) => /^GIT_/i.test(name) || ANSWER_VARIABLES.includes(name.toUpperCase()))
190
+ // One case for each name, whichever: the mutant that folds to the other is equivalent.
191
+ .map((name) => [name.toUpperCase(), env[name]])
192
+ .sort();
193
+ return JSON.stringify({ root, session, command, version, node: process.version, args: LIST, variables });
194
+ }
195
+ /** The siblings of the work tree at `root`, looked for in its install and then in each of `installs` (locate). */
196
+ export function createSiblings(root, config, installs = []) {
197
+ return siblingsOf(root, config, installs, null);
198
+ }
199
+ function siblingsOf(root, config, installs, kept) {
114
200
  return {
115
201
  root,
116
- locate: (name) => locate(name, root, config),
117
- async briefs() {
118
- const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);
202
+ locate: (name) => locate(name, root, config, installs),
203
+ async briefs(limit = ANSWER_MS) {
204
+ const sibling = locate('spec-brief', root, config, installs);
205
+ if (sibling.kind !== 'found')
206
+ throw new SiblingError(sibling.reason);
207
+ const keeper = kept === null ? null : await keeperOf(kept, root, sibling);
208
+ if (keeper !== null) {
209
+ const again = await givenAgain(await keptAnswer(keeper.commonDir, keeper.name, keeper.key), keeper.version, root, keeper.hash);
210
+ if (again !== null)
211
+ return again;
212
+ }
213
+ const run = await runSibling('spec-brief', sibling.command, LIST, root, limit);
119
214
  if (run.code === 2) {
120
215
  throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\n')[0] || 'exit 2'}`);
121
216
  }
217
+ let document;
218
+ let rows;
122
219
  try {
123
- return parseBriefList(parseJson('spec-brief', run));
220
+ document = parseJson('spec-brief', run);
221
+ rows = parseBriefList(document);
124
222
  }
125
223
  catch (error) {
126
224
  // parseJson throws a SiblingError, which this would copy, and
@@ -130,12 +228,19 @@ export function createSiblings(root, config) {
130
228
  throw new SiblingError(error.message);
131
229
  throw error;
132
230
  }
231
+ // Kept only where spec-brief said all was well and named what it read,
232
+ // in the version that answered: any other answer is read once.
233
+ if (keeper !== null && run.code === 0 && inputsOf(document)?.tool === keeper.version) {
234
+ await keepAnswer(keeper.commonDir, keeper.name, keeper.key, run.stdout);
235
+ }
236
+ return rows;
133
237
  },
238
+ keeping: (commonDir, session) => siblingsOf(root, config, installs, { commonDir, session }),
134
239
  async json(name, args) {
135
- const sibling = locate(name, root, config);
240
+ const sibling = locate(name, root, config, installs);
136
241
  if (sibling.kind !== 'found')
137
242
  return { absent: sibling.reason };
138
- const run = await runSibling(sibling.command, args, root);
243
+ const run = await runSibling(name, sibling.command, args, root);
139
244
  return { code: run.code, document: parseJson(name, run), stderr: run.stderr };
140
245
  },
141
246
  };
@@ -1 +1 @@
1
- {"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /** `version` is `null` for a command `.spec-harness.json` names, whose version is not checked. */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null };\n\n/** Where a sibling is, or why it cannot be run. */\nexport function locate(name: SiblingName, root: string, config: HarnessConfig): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n const directory = join(root, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) {\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n }\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version };\n return { kind: 'found', command: [process.execPath, bin], version: check.version };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nexport function declaredVersion(file: string): unknown {\n // A manifest that holds null throws on the read, and any other value that\n // is not an object has no version, so the catch answers for both. Falling\n // out of it answers undefined as well, and a Buffer parses as its text:\n // the empty catch and the encoding are equivalent to their mutants.\n try {\n const manifest: { version?: unknown } = JSON.parse(readFileSync(file, 'utf8'));\n return manifest.version;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n */\nexport function runSibling(command: readonly string[], args: readonly string[], root: string, timeoutMs = 600_000): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n // Keeps a console window from opening on Windows; the sweep runs on\n // Linux, where the mutant is equivalent.\n windowsHide: true,\n timeout: timeoutMs,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */\n briefs(): Promise<BriefRow[]>;\n /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown; readonly stderr: string } | { readonly absent: string }>;\n}\n\nexport function createSiblings(root: string, config: HarnessConfig): Siblings {\n const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n return sibling.command;\n };\n return {\n root,\n locate: (name) => locate(name, root, config),\n async briefs() {\n const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] || 'exit 2'}`);\n }\n try {\n return parseBriefList(parseJson('spec-brief', run));\n } catch (error) {\n // parseJson throws a SiblingError, which this would copy, and\n // parseBriefList nothing but its own: the check is equivalent to its\n // mutant, and keeps any other error the stack trace it needs.\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n },\n async json(name, args) {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run), stderr: run.stderr };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { inputsOf, parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport { hasher, isFile, keepAnswer, keptAnswer, readsAsListed, type Hash } from './fs.js';\nimport { ANSWER_MS, stoppedAtLimit, WORK_MS } from './git.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /**\n * `version` is `null` for a command `.spec-harness.json` names, whose\n * version is not checked. `elsewhere` is the directory whose install it\n * is, where that is not the work tree's own.\n */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null; readonly elsewhere?: string }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null; readonly elsewhere?: string };\n\n/**\n * Where a sibling is, or why it cannot be run. It is looked for in `root`'s\n * install, then in each of `installs` in turn, and the first that has it\n * decides, at this release's floor: a work tree with an install of its own\n * runs it, outdated or not, and one with none is checked with the\n * project's, as git's hook and Claude Code's are where a linked worktree has\n * none (ADR-0005).\n */\n// A name a mutant puts on the default list is a directory relative to where\n// the run is, which holds no install: equivalent, here and below.\nexport function locate(name: SiblingName, root: string, config: HarnessConfig, installs: readonly string[] = []): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n for (const place of [root, ...installs]) {\n const directory = join(place, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) continue;\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n const where = place === root ? {} : { elsewhere: place };\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version, ...where };\n return { kind: 'found', command: [process.execPath, bin], version: check.version, ...where };\n }\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nexport function declaredVersion(file: string): unknown {\n // Only a file is read: a named pipe under the name was read without end,\n // and before any sibling ran (ADR-0002).\n if (!isFile(file)) return undefined;\n // A manifest that holds null throws on the read, and any other value that\n // is not an object has no version, so the catch answers for both. Falling\n // out of it answers undefined as well, and a Buffer parses as its text:\n // the empty catch and the encoding are equivalent to their mutants.\n try {\n const manifest: { version?: unknown } = JSON.parse(readFileSync(file, 'utf8'));\n return manifest.version;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n *\n * A sibling that does not answer within `limit` is stopped, and rejected as\n * a `SiblingError` that names it: what it was asked waits on it, the hook\n * before a write among them, which then holds the write (ADR-0002).\n */\nexport function runSibling(name: string, command: readonly string[], args: readonly string[], root: string, limit = WORK_MS): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve, reject) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n // Keeps a console window from opening on Windows; the sweep runs on\n // Linux, where the mutant is equivalent.\n windowsHide: true,\n timeout: limit,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n if (stoppedAtLimit(error)) {\n reject(new SiblingError(`${name} did not answer within ${limit / 1000} seconds, and was stopped`));\n return;\n }\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\n/**\n * Where and under what a session's answer of spec-brief's is kept, or\n * `null` where none is. A command `tools` names is started every time:\n * which version it runs cannot be told without starting it.\n */\nasync function keeperOf(kept: Kept, root: string, sibling: Extract<Sibling, { kind: 'found' }>): Promise<{ readonly commonDir: string; readonly key: string; readonly name: string; readonly version: string; readonly hash: Hash } | null> {\n // A command with no version is kept nothing by the test of the version an\n // answer names, too: the mutant without this one reads, for every write, a\n // kept answer there is none of, and is equivalent.\n if (sibling.version === null) return null;\n const hash = await hasher();\n const key = answerKey(root, kept.session, sibling.command, sibling.version);\n return { commonDir: kept.commonDir, key, name: `${hash(key)}.json`, version: sibling.version, hash };\n}\n\n/**\n * The briefs a kept answer gives, or `null` where it is not to be given:\n * nothing kept, or what was kept is not a list document that names what it\n * was read from in the version installed, or anything it names reads\n * otherwise.\n */\nasync function givenAgain(stdout: string | null, version: string, root: string, hash: Hash): Promise<BriefRow[] | null> {\n // JSON.parse reads null as null, which names no inputs, and a null read\n // past the test of none below throws, which answers null: the two tests\n // are the compiler's, and their mutants equivalent.\n if (stdout === null) return null;\n try {\n const document: unknown = JSON.parse(stdout);\n const inputs = inputsOf(document);\n if (inputs === null || inputs.tool !== version || !(await readsAsListed(root, inputs, hash))) return null;\n return parseBriefList(document);\n } catch {\n // What another program wrote there is no answer of spec-brief's.\n return null;\n }\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /**\n * Every brief, live and archived. Exit 2 from spec-brief is an error here\n * too, and so is a spec-brief that does not answer within `limit`, the\n * time a question is given unless a caller gives another.\n */\n briefs(limit?: number): Promise<BriefRow[]>;\n /**\n * The same siblings, with spec-brief's list of the briefs kept for a\n * session under the common directory and given again to that session's\n * runs where every input it lists reads as listed (ADR-0002).\n */\n keeping(commonDir: string, session: string): Siblings;\n /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown; readonly stderr: string } | { readonly absent: string }>;\n}\n\n/** The command line spec-brief is asked for the briefs with. */\nconst LIST: readonly string[] = ['list', '--archived', '--format', 'json', '--no-color'];\n\n/**\n * The variables beyond git's own that an answer of spec-brief's can turn on\n * and its list does not name: which git and which Node run, where git finds\n * the person's configuration and ignore files, and how Node runs a script.\n * Every variable whose name begins `GIT_` is read with them.\n */\nconst ANSWER_VARIABLES: readonly string[] = ['PATH', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'XDG_CONFIG_HOME', 'NODE_OPTIONS', 'NODE_PATH'];\n\n/**\n * What a kept answer is the answer to: the work tree, the session, the\n * spec-brief that answered, by its command, its version and the Node it ran\n * on, the command line, and the variables above, as spec-brief is started\n * with them (runSibling). An answer is given again only for the same.\n */\nexport function answerKey(root: string, session: string, command: readonly string[], version: string, env: Readonly<Record<string, string | undefined>> = process.env): string {\n const variables = Object.keys(env)\n .filter((name) => /^GIT_/i.test(name) || ANSWER_VARIABLES.includes(name.toUpperCase()))\n // One case for each name, whichever: the mutant that folds to the other is equivalent.\n .map((name) => [name.toUpperCase(), env[name]])\n .sort();\n return JSON.stringify({ root, session, command, version, node: process.version, args: LIST, variables });\n}\n\n/** The siblings of the work tree at `root`, looked for in its install and then in each of `installs` (locate). */\nexport function createSiblings(root: string, config: HarnessConfig, installs: readonly string[] = []): Siblings {\n return siblingsOf(root, config, installs, null);\n}\n\n/** Where a session's answers are kept, and which session's they are. */\ninterface Kept {\n readonly commonDir: string;\n readonly session: string;\n}\n\nfunction siblingsOf(root: string, config: HarnessConfig, installs: readonly string[], kept: Kept | null): Siblings {\n return {\n root,\n locate: (name) => locate(name, root, config, installs),\n async briefs(limit = ANSWER_MS) {\n const sibling = locate('spec-brief', root, config, installs);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n const keeper = kept === null ? null : await keeperOf(kept, root, sibling);\n if (keeper !== null) {\n const again = await givenAgain(await keptAnswer(keeper.commonDir, keeper.name, keeper.key), keeper.version, root, keeper.hash);\n if (again !== null) return again;\n }\n const run = await runSibling('spec-brief', sibling.command, LIST, root, limit);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] || 'exit 2'}`);\n }\n let document: unknown;\n let rows: BriefRow[];\n try {\n document = parseJson('spec-brief', run);\n rows = parseBriefList(document);\n } catch (error) {\n // parseJson throws a SiblingError, which this would copy, and\n // parseBriefList nothing but its own: the check is equivalent to its\n // mutant, and keeps any other error the stack trace it needs.\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n // Kept only where spec-brief said all was well and named what it read,\n // in the version that answered: any other answer is read once.\n if (keeper !== null && run.code === 0 && inputsOf(document)?.tool === keeper.version) {\n await keepAnswer(keeper.commonDir, keeper.name, keeper.key, run.stdout);\n }\n return rows;\n },\n keeping: (commonDir, session) => siblingsOf(root, config, installs, { commonDir, session }),\n async json(name, args) {\n const sibling = locate(name, root, config, installs);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(name, sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run), stderr: run.stderr };\n },\n };\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -36,6 +36,11 @@ export interface BriefRow {
36
36
  readonly ready: boolean;
37
37
  readonly waitingOn: readonly string[];
38
38
  }
39
+ /** A local branch: its name, and the short name git reads as it and nothing else, as a base is written. */
40
+ export interface LocalBranch {
41
+ readonly name: string;
42
+ readonly ref: string;
43
+ }
39
44
  /** A file a range of commits changed, as `git diff --name-status` reports it. */
40
45
  export interface FileChange {
41
46
  readonly status: 'A' | 'M' | 'D' | 'R' | 'C' | 'T';
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,mEAAmE","sourcesContent":["/** Types shared across modules. Type-only: compiles to nothing. */\n\nexport type Severity = 'error' | 'warning' | 'note';\n\n/** One thing a command found, where to fix it, and what to do next (spec-core ADR-0005). */\nexport interface Finding {\n readonly rule: string;\n readonly severity: Severity;\n readonly message: string;\n readonly hint: string;\n readonly file?: string | undefined;\n readonly line?: number | undefined;\n /**\n * What the finding is about - an assertion, a ruling, a dependency, a name -\n * in words that stay the same while it stays the same finding. With the rule\n * and the file it is the finding's identity, which a GitLab fingerprint is\n * made of; the message, the hint and the line never are.\n */\n readonly subject?: string | undefined;\n}\n\n/** A brief as `spec-brief list --format json` reports it. */\nexport interface BriefRow {\n readonly id: string;\n /** Repository-relative POSIX path. */\n readonly file: string;\n readonly title: string | null;\n readonly phase: 'live' | 'archived';\n readonly status: string | null;\n readonly type: string | null;\n readonly wave: number | null;\n readonly dependsOn: readonly string[];\n readonly affectedFiles: readonly string[];\n readonly protectedFiles: readonly string[];\n readonly tasks: { readonly total: number; readonly checked: number };\n readonly ready: boolean;\n readonly waitingOn: readonly string[];\n}\n\n/** A file a range of commits changed, as `git diff --name-status` reports it. */\nexport interface FileChange {\n readonly status: 'A' | 'M' | 'D' | 'R' | 'C' | 'T';\n readonly path: string;\n /** The path it came from, for a rename or a copy. */\n readonly from?: string | undefined;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,mEAAmE","sourcesContent":["/** Types shared across modules. Type-only: compiles to nothing. */\n\nexport type Severity = 'error' | 'warning' | 'note';\n\n/** One thing a command found, where to fix it, and what to do next (spec-core ADR-0005). */\nexport interface Finding {\n readonly rule: string;\n readonly severity: Severity;\n readonly message: string;\n readonly hint: string;\n readonly file?: string | undefined;\n readonly line?: number | undefined;\n /**\n * What the finding is about - an assertion, a ruling, a dependency, a name -\n * in words that stay the same while it stays the same finding. With the rule\n * and the file it is the finding's identity, which a GitLab fingerprint is\n * made of; the message, the hint and the line never are.\n */\n readonly subject?: string | undefined;\n}\n\n/** A brief as `spec-brief list --format json` reports it. */\nexport interface BriefRow {\n readonly id: string;\n /** Repository-relative POSIX path. */\n readonly file: string;\n readonly title: string | null;\n readonly phase: 'live' | 'archived';\n readonly status: string | null;\n readonly type: string | null;\n readonly wave: number | null;\n readonly dependsOn: readonly string[];\n readonly affectedFiles: readonly string[];\n readonly protectedFiles: readonly string[];\n readonly tasks: { readonly total: number; readonly checked: number };\n readonly ready: boolean;\n readonly waitingOn: readonly string[];\n}\n\n/** A local branch: its name, and the short name git reads as it and nothing else, as a base is written. */\nexport interface LocalBranch {\n readonly name: string;\n readonly ref: string;\n}\n\n/** A file a range of commits changed, as `git diff --name-status` reports it. */\nexport interface FileChange {\n readonly status: 'A' | 'M' | 'D' | 'R' | 'C' | 'T';\n readonly path: string;\n /** The path it came from, for a rename or a copy. */\n readonly from?: string | undefined;\n}\n"]}
package/dist/usage.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface CommandLine {
19
19
  readonly positionals: readonly string[];
20
20
  /** The long name of every option on it, in the order given. */
21
21
  readonly given: readonly string[];
22
+ /** Every value of each option that takes one, by its long name, in the order given; the fields below hold the last. */
23
+ readonly written: Readonly<Record<string, readonly string[]>>;
22
24
  readonly brief: string | undefined;
23
25
  readonly base: string | undefined;
24
26
  readonly root: string | undefined;
@@ -28,6 +30,9 @@ export interface CommandLine {
28
30
  readonly recommend: string | undefined;
29
31
  readonly show: string | undefined;
30
32
  readonly id: string | undefined;
33
+ /** Whether the line asks for the help, or for the version, which every command answers in place of running. */
34
+ readonly help: boolean;
35
+ readonly version: boolean;
31
36
  }
32
37
  /** Whether a value names nothing: empty, or only space. */
33
38
  export declare function isBlank(value: string): boolean;
@@ -75,6 +80,28 @@ declare const VALUED: {
75
80
  readonly says: "names no probe; give it a probe's id, or leave it out to run every probe";
76
81
  };
77
82
  };
83
+ /**
84
+ * A resolved path with its drive letter in one case. On Windows a drive
85
+ * letter names its volume in either case, `D:\` and `d:\` alike (Microsoft's
86
+ * Naming Files, Paths, and Namespaces, Naming Conventions, read 2026-10-19),
87
+ * where Node's `resolve` keeps the case it was written in (spec-core
88
+ * ADR-0005, amended 2026-10-19). The rest of the path is left as it is: NTFS
89
+ * can be made to tell names apart by case, so whether two names are one
90
+ * directory is the disk's to say. A path resolved elsewhere starts with `/`,
91
+ * and has no drive letter to fold.
92
+ */
93
+ export declare function foldDrive(path: string): string;
94
+ /**
95
+ * The first option that takes one value and was given two that read as two,
96
+ * named with both as they were written; `null` where none was. The parser
97
+ * keeps the last, and the earlier went unread with no word of it:
98
+ * `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001`
99
+ * was brief 001 (spec-core ADR-0005). The same value given twice, however it
100
+ * is spelled, is the option once, and the last is the one read. `cwd` is
101
+ * where a relative root is read from. A list, `--path` and `--option`, has
102
+ * every value it is given.
103
+ */
104
+ export declare function twoValues(line: Pick<CommandLine, 'written'>, cwd: string): string | null;
78
105
  export type ValuedOption = keyof typeof VALUED;
79
106
  /**
80
107
  * The line for a value that names nothing. `subject` is the input as its
@@ -190,7 +217,7 @@ declare const SHAPE: {
190
217
  readonly most: 0;
191
218
  readonly takes: "no argument";
192
219
  };
193
- readonly options: readonly ["root", "format", "write", "git-hook"];
220
+ readonly options: readonly ["root", "format", "write", "git-hook", "block-on-failure"];
194
221
  };
195
222
  readonly mcp: {
196
223
  readonly limit: {
@@ -217,7 +244,7 @@ export type Command = keyof typeof SHAPE;
217
244
  * every release, which this one cannot rewrite, and exit 2 from it holds
218
245
  * every write of a session, the write that would fix the line among them.
219
246
  * A line the parser cannot read is asked the same, and the hook says it
220
- * with exit 1 (cli.ts).
247
+ * to the person with exit 0 (cli.ts).
221
248
  */
222
249
  export declare function isClaudeHook(command: string | undefined, argument: string | undefined): boolean;
223
250
  /** The words for a `--root` that is a file, or is not there, wherever it is said. */
@@ -231,7 +258,7 @@ export interface LineFault {
231
258
  * What Claude Code's hook does with its own line, as far as the line decides
232
259
  * it: it guards, in the root the line gives it where it gives one and under
233
260
  * the brief it names where it names one; or the guard is off, for a fault
234
- * the hook says with exit 1 and holds no write for; or every write is held,
261
+ * the hook says with exit 0 and holds no write for; or every write is held,
235
262
  * exit 2, for a brief the line names that is not one (ADR-0005). The hook
236
263
  * reads its line through the functions below, and `doctor` reads a line it
237
264
  * finds in Claude Code's settings through the same ones, so what it says of
@@ -271,9 +298,9 @@ export interface GuardOff {
271
298
  readonly state: 'off';
272
299
  readonly fault: LineFault;
273
300
  /**
274
- * Set where the hook says nothing of the fault and exits 0, so that no
275
- * run of it fails: a root outside any work tree, or in a work tree that
276
- * does not hold the project. Every other fault it says, with exit 1.
301
+ * Set where the hook says nothing of the fault: a root outside any work
302
+ * tree, or in a work tree that does not hold the project. Every other
303
+ * fault it says, to the person, and every one with exit 0.
277
304
  */
278
305
  readonly silent?: true;
279
306
  /**
@@ -297,9 +324,11 @@ export interface WritesHeld {
297
324
  export declare function heldHookLine(wrong: string): WritesHeld;
298
325
  /**
299
326
  * A line the parser refused: an option no command has, an option left
300
- * without its value, a `--format` or an `--at` that is none. The parser's
301
- * words are kept, on one line: what node says of an option that may have
302
- * lost its value is three.
327
+ * without its value, a `--format` or an `--at` that is none, `--help` beside
328
+ * `--version`; or a line it read with two values for an option that takes
329
+ * one, of which it cannot tell the one meant ({@link twoValues}). The
330
+ * parser's words are kept, on one line: what node says of an option that may
331
+ * have lost its value is three.
303
332
  */
304
333
  export declare function unreadHookLine(said: string): GuardOff;
305
334
  /**
@@ -311,6 +340,25 @@ export declare function unreadHookLine(said: string): GuardOff;
311
340
  * someone had configured was off.
312
341
  */
313
342
  export declare function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff;
343
+ /**
344
+ * What `--help` or `--version` on the hook's line stands beside that the
345
+ * hook would say as it runs, in the words for a line it cannot read; `null`
346
+ * where the line asks for neither, or stands beside nothing the line itself
347
+ * shows to be wrong. Every command answers either before it runs, so the
348
+ * hook printed the help over a root that is not a directory, exit 0, and
349
+ * said nothing of it (ADR-0005). A fault only git or spec-brief can tell
350
+ * stays unsaid: neither is asked before an answer that runs nothing.
351
+ */
352
+ export declare function answerBeside(line: Pick<CommandLine, 'help' | 'version' | 'root'>, cwd: string, isDirectory: (path: string) => boolean): string | null;
353
+ /**
354
+ * A line of the hook's that asks for the help or the version, which the hook
355
+ * prints, exit 0, in place of reading its question: no write is checked
356
+ * under it, and Claude Code, by its hooks reference, writes what a hook
357
+ * that exits 0 prints to its debug log, so nothing says the guard is off
358
+ * (ADR-0005). The hook says nothing of it, since a person who asks the hook
359
+ * for its help at a terminal is answered the same way.
360
+ */
361
+ export declare function answeringHookLine(asked: 'help' | 'version'): GuardOff;
314
362
  /**
315
363
  * A line whose root is a directory outside any git work tree. The hook
316
364
  * reads that as a session outside one, where it has nothing to guard: it
@@ -350,15 +398,21 @@ export declare function holderHookLine(line: Guards, tree: OtherTree, path: stri
350
398
  * A line that guards says which brief it names, where it names one: the hook
351
399
  * passes over a `--brief` that is empty, as every release has, and holds
352
400
  * every write under one that names no brief, which only spec-brief can tell.
401
+ * A line with two values for an option that takes one is a line the hook
402
+ * cannot read, whichever option it is, read from `cwd` as the hook reads it
403
+ * from where it is started (cli.ts). So is one with the help or the version
404
+ * beside a root that is not a directory; and with either beside nothing it
405
+ * can see is wrong, the line checks no write, whatever else it holds.
353
406
  */
354
407
  export declare function claudeHookLine(words: readonly string[], parse: (argv: readonly string[]) => CommandLine, cwd: string, isDirectory: (path: string) => boolean): HookLine | null;
355
408
  /**
356
409
  * Why the command line is refused, or `null` when the command can run:
357
410
  * an option it does not read, alone or beside another, an argument it does
358
411
  * not take, a brief named twice, a value or an argument that names nothing,
359
- * a `--root` that is no directory. Asked once a command is known and neither
360
- * `--help` nor `--version` answers instead; a command with no shape is none,
361
- * which its caller says.
412
+ * a `--root` that is no directory. Asked once a command is known, and before
413
+ * `--help` or `--version` answers: a line that is wrong is refused with
414
+ * either on it, as one the parser refuses always was (spec-core ADR-0005).
415
+ * A command with no shape is none, which its caller says.
362
416
  */
363
417
  export declare function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null;
364
418
  export {};