@descent-vtt/spec-harness 0.19.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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +212 -0
  4. package/README.md +30 -15
  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 +211 -134
  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 +13 -1
  14. package/dist/config.js +14 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/configure.d.ts +55 -1
  17. package/dist/configure.js +130 -22
  18. package/dist/configure.js.map +1 -1
  19. package/dist/fs.d.ts +78 -1
  20. package/dist/fs.js +289 -10
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +50 -2
  23. package/dist/git.js +98 -8
  24. package/dist/git.js.map +1 -1
  25. package/dist/hooks.d.ts +45 -0
  26. package/dist/hooks.js +62 -3
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/round.d.ts +8 -0
  29. package/dist/round.js +23 -8
  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.js +40 -10
  37. package/dist/setup.js.map +1 -1
  38. package/dist/siblings.d.ts +24 -3
  39. package/dist/siblings.js +106 -14
  40. package/dist/siblings.js.map +1 -1
  41. package/dist/usage.d.ts +42 -7
  42. package/dist/usage.js +65 -6
  43. package/dist/usage.js.map +1 -1
  44. package/dist/workspace.d.ts +45 -3
  45. package/dist/workspace.js +133 -13
  46. package/dist/workspace.js.map +1 -1
  47. package/package.json +1 -1
@@ -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 /**\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 // 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\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 const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config, installs);\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, installs),\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, installs);\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/usage.d.ts CHANGED
@@ -30,6 +30,9 @@ export interface CommandLine {
30
30
  readonly recommend: string | undefined;
31
31
  readonly show: string | undefined;
32
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;
33
36
  }
34
37
  /** Whether a value names nothing: empty, or only space. */
35
38
  export declare function isBlank(value: string): boolean;
@@ -77,6 +80,17 @@ declare const VALUED: {
77
80
  readonly says: "names no probe; give it a probe's id, or leave it out to run every probe";
78
81
  };
79
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;
80
94
  /**
81
95
  * The first option that takes one value and was given two that read as two,
82
96
  * named with both as they were written; `null` where none was. The parser
@@ -203,7 +217,7 @@ declare const SHAPE: {
203
217
  readonly most: 0;
204
218
  readonly takes: "no argument";
205
219
  };
206
- readonly options: readonly ["root", "format", "write", "git-hook"];
220
+ readonly options: readonly ["root", "format", "write", "git-hook", "block-on-failure"];
207
221
  };
208
222
  readonly mcp: {
209
223
  readonly limit: {
@@ -230,7 +244,7 @@ export type Command = keyof typeof SHAPE;
230
244
  * every release, which this one cannot rewrite, and exit 2 from it holds
231
245
  * every write of a session, the write that would fix the line among them.
232
246
  * A line the parser cannot read is asked the same, and the hook says it
233
- * with exit 1 (cli.ts).
247
+ * to the person with exit 0 (cli.ts).
234
248
  */
235
249
  export declare function isClaudeHook(command: string | undefined, argument: string | undefined): boolean;
236
250
  /** The words for a `--root` that is a file, or is not there, wherever it is said. */
@@ -244,7 +258,7 @@ export interface LineFault {
244
258
  * What Claude Code's hook does with its own line, as far as the line decides
245
259
  * it: it guards, in the root the line gives it where it gives one and under
246
260
  * the brief it names where it names one; or the guard is off, for a fault
247
- * 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,
248
262
  * exit 2, for a brief the line names that is not one (ADR-0005). The hook
249
263
  * reads its line through the functions below, and `doctor` reads a line it
250
264
  * finds in Claude Code's settings through the same ones, so what it says of
@@ -284,9 +298,9 @@ export interface GuardOff {
284
298
  readonly state: 'off';
285
299
  readonly fault: LineFault;
286
300
  /**
287
- * Set where the hook says nothing of the fault and exits 0, so that no
288
- * run of it fails: a root outside any work tree, or in a work tree that
289
- * 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.
290
304
  */
291
305
  readonly silent?: true;
292
306
  /**
@@ -326,6 +340,25 @@ export declare function unreadHookLine(said: string): GuardOff;
326
340
  * someone had configured was off.
327
341
  */
328
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;
329
362
  /**
330
363
  * A line whose root is a directory outside any git work tree. The hook
331
364
  * reads that as a session outside one, where it has nothing to guard: it
@@ -367,7 +400,9 @@ export declare function holderHookLine(line: Guards, tree: OtherTree, path: stri
367
400
  * every write under one that names no brief, which only spec-brief can tell.
368
401
  * A line with two values for an option that takes one is a line the hook
369
402
  * cannot read, whichever option it is, read from `cwd` as the hook reads it
370
- * from where it is started (cli.ts).
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.
371
406
  */
372
407
  export declare function claudeHookLine(words: readonly string[], parse: (argv: readonly string[]) => CommandLine, cwd: string, isDirectory: (path: string) => boolean): HookLine | null;
373
408
  /**
package/dist/usage.js CHANGED
@@ -44,19 +44,33 @@ const VALUED = literal({
44
44
  show: { of: every('show'), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },
45
45
  id: { of: every('id'), says: "names no probe; give it a probe's id, or leave it out to run every probe" },
46
46
  });
47
+ /**
48
+ * A resolved path with its drive letter in one case. On Windows a drive
49
+ * letter names its volume in either case, `D:\` and `d:\` alike (Microsoft's
50
+ * Naming Files, Paths, and Namespaces, Naming Conventions, read 2026-10-19),
51
+ * where Node's `resolve` keeps the case it was written in (spec-core
52
+ * ADR-0005, amended 2026-10-19). The rest of the path is left as it is: NTFS
53
+ * can be made to tell names apart by case, so whether two names are one
54
+ * directory is the disk's to say. A path resolved elsewhere starts with `/`,
55
+ * and has no drive letter to fold.
56
+ */
57
+ export function foldDrive(path) {
58
+ return /^[a-z]:/.test(path) ? `${path[0].toUpperCase()}${path.slice(1)}` : path;
59
+ }
47
60
  /**
48
61
  * Whether two values of an option that takes one ask for one thing, as the
49
62
  * option reads them: a brief by its id as spec-brief's names are matched,
50
63
  * space around it dropped and `12` the brief `012` (ADR-0004); a root by the
51
64
  * directory it names from `cwd`, the one an empty root names on the hook's
52
- * line among them; any other as it is written, since each is read so, a
53
- * format, a probe's position, an id or text.
65
+ * line among them, with a drive letter in either case; any other as it is
66
+ * written, since each is read so, a format, a probe's position, an id or
67
+ * text.
54
68
  */
55
69
  function asksOne(option, first, other, cwd) {
56
70
  if (option === 'brief')
57
71
  return sameId(first.trim(), other.trim());
58
72
  if (option === 'root')
59
- return resolve(cwd, first) === resolve(cwd, other);
73
+ return foldDrive(resolve(cwd, first)) === foldDrive(resolve(cwd, other));
60
74
  return first === other;
61
75
  }
62
76
  /**
@@ -125,7 +139,7 @@ const SHAPE = literal({
125
139
  audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },
126
140
  probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },
127
141
  premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },
128
- init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },
142
+ init: { options: ['root', 'format', 'write', 'git-hook', 'block-on-failure'], ...NO_ARGUMENT },
129
143
  mcp: { options: ['root'], ...NO_ARGUMENT },
130
144
  // A diagnosis says what is wrong with what it was given, where a command
131
145
  // that acts on it stops: a brief or a base that names nothing is a line of
@@ -151,7 +165,7 @@ function listed(options) {
151
165
  * every release, which this one cannot rewrite, and exit 2 from it holds
152
166
  * every write of a session, the write that would fix the line among them.
153
167
  * A line the parser cannot read is asked the same, and the hook says it
154
- * with exit 1 (cli.ts).
168
+ * to the person with exit 0 (cli.ts).
155
169
  */
156
170
  export function isClaudeHook(command, argument) {
157
171
  return command === 'hook' && argument === 'claude';
@@ -199,6 +213,42 @@ export function readHookLine(root, cwd, isDirectory) {
199
213
  }
200
214
  return { state: 'guards', root: given };
201
215
  }
216
+ /**
217
+ * What `--help` or `--version` on the hook's line stands beside that the
218
+ * hook would say as it runs, in the words for a line it cannot read; `null`
219
+ * where the line asks for neither, or stands beside nothing the line itself
220
+ * shows to be wrong. Every command answers either before it runs, so the
221
+ * hook printed the help over a root that is not a directory, exit 0, and
222
+ * said nothing of it (ADR-0005). A fault only git or spec-brief can tell
223
+ * stays unsaid: neither is asked before an answer that runs nothing.
224
+ */
225
+ export function answerBeside(line, cwd, isDirectory) {
226
+ if (!line.help && !line.version)
227
+ return null;
228
+ const read = readHookLine(line.root, cwd, isDirectory);
229
+ if (read.state === 'guards')
230
+ return null;
231
+ const asked = line.help ? 'help' : 'version';
232
+ return `--${asked} is given beside a fault, ${read.fault.wrong}, and the ${asked} would be printed in its place`;
233
+ }
234
+ /**
235
+ * A line of the hook's that asks for the help or the version, which the hook
236
+ * prints, exit 0, in place of reading its question: no write is checked
237
+ * under it, and Claude Code, by its hooks reference, writes what a hook
238
+ * that exits 0 prints to its debug log, so nothing says the guard is off
239
+ * (ADR-0005). The hook says nothing of it, since a person who asks the hook
240
+ * for its help at a terminal is answered the same way.
241
+ */
242
+ export function answeringHookLine(asked) {
243
+ return {
244
+ state: 'off',
245
+ silent: true,
246
+ fault: {
247
+ wrong: `the line asks for the ${asked}, which the hook prints, exit 0, in place of checking the write`,
248
+ mend: 'take that option off the line, where init writes "hook claude" and nothing after it',
249
+ },
250
+ };
251
+ }
202
252
  /**
203
253
  * A line whose root is a directory outside any git work tree. The hook
204
254
  * reads that as a session outside one, where it has nothing to guard: it
@@ -261,7 +311,9 @@ export function holderHookLine(line, tree, path) {
261
311
  * every write under one that names no brief, which only spec-brief can tell.
262
312
  * A line with two values for an option that takes one is a line the hook
263
313
  * cannot read, whichever option it is, read from `cwd` as the hook reads it
264
- * from where it is started (cli.ts).
314
+ * from where it is started (cli.ts). So is one with the help or the version
315
+ * beside a root that is not a directory; and with either beside nothing it
316
+ * can see is wrong, the line checks no write, whatever else it holds.
265
317
  */
266
318
  export function claudeHookLine(words, parse, cwd, isDirectory) {
267
319
  let line;
@@ -277,6 +329,13 @@ export function claudeHookLine(words, parse, cwd, isDirectory) {
277
329
  const twice = twoValues(line, cwd);
278
330
  if (twice !== null)
279
331
  return unreadHookLine(twice);
332
+ const beside = answerBeside(line, cwd, isDirectory);
333
+ if (beside !== null)
334
+ return unreadHookLine(beside);
335
+ if (line.help)
336
+ return answeringHookLine('help');
337
+ if (line.version)
338
+ return answeringHookLine('version');
280
339
  const read = readHookLine(line.root, cwd, isDirectory);
281
340
  return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;
282
341
  }
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAqBvC,2DAA2D;AAC3D,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAQD,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,iBAAiB;AACjB,MAAM,KAAK,GACT,CAAC,MAAc,EAAE,EAAE,CACnB,CAAC,IAAiB,EAAqB,EAAE,CACvC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,GAAG,OAAO,CAAC;IACrB,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,kGAAkG,EAAE;IACvI,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,wFAAwF,EAAE;IAC3H,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,kFAAkF,EAAE;IACrH,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,2CAA2C,EAAE;IACrF,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,oEAAoE,EAAE;IAC3G,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,6DAA6D,EAAE;IAC3G,SAAS,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,+DAA+D,EAAE;IAC5G,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iFAAiF,EAAE;IACpH,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,0EAA0E,EAAE;CAC1G,CAAkC,CAAC;AAEpC;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,MAAc,EAAE,KAAa,EAAE,KAAa,EAAE,GAAW;IACxE,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC1E,OAAO,KAAK,KAAK,KAAK,CAAC;AACzB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkC,EAAE,GAAW;IACvE,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1E,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,MAAM,uBAAuB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,sCAAsC,CAAC;IAChK,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,MAAoB,EAAE,KAAa;IAC7E,OAAO,GAAG,OAAO,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;AAChF,CAAC;AAwBD,MAAM,WAAW,GAAG,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAC1E,MAAM,SAAS,GAAG,OAAO,CAAC;IACxB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE;IAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,2FAA2F,EAAE,MAAM,EAAE,OAAO,EAAE;CACvJ,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,KAAK,GAAG,OAAO,CAAC;IACpB,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IACvE,KAAK,EAAE;QACL,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;QACtD,KAAK,EAAE,IAAI;QACX,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,yCAAyC,EAAE;KACjF;IACD,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,iCAAiC,EAAE,EAAE;IAC1G,0EAA0E;IAC1E,0EAA0E;IAC1E,QAAQ,EAAE;QACR,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC;QAC7F,GAAG,WAAW;QACd,KAAK,EAAE;YACL,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;YAC9C,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;SAC/C;KACF;IACD,IAAI,EAAE;QACJ,OAAO,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC;QACpD,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,mCAAmC,EAAE;QAC9D,QAAQ,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,4DAA4D,EAAE;KAC5G;IACD,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IACvE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IAC/E,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,SAAS,EAAE;IACjF,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,WAAW,EAAE;IAC5E,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,GAAG,WAAW,EAAE;IAC1E,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,WAAW,EAAE;IAC1C,yEAAyE;IACzE,2EAA2E;IAC3E,mBAAmB;IACnB,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,WAAW,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE;CAC/G,CAAiC,CAAC;AAEnC,kFAAkF;AAClF,MAAM,OAAO,GAAsB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAKvD,2EAA2E;AAC3E,4DAA4D;AAC5D,MAAM,MAAM,GAA+B,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAE1E,SAAS,MAAM,CAAC,OAA0B;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;IACrD,mEAAmE;IACnE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,sBAAsB,KAAK,CAAC,CAAC,CAAW,EAAE,CAAC;IAC1E,OAAO,mBAAmB,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAA2B,EAAE,QAA4B;IACpF,OAAO,OAAO,KAAK,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC;AACrD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,cAAc,IAAI,6BAA6B,CAAC;AACzD,CAAC;AA+CD,kEAAkE;AAClE,MAAM,UAAU,SAAS,CAAC,EAAE,GAAG,EAAE,IAAI,EAAa;IAChD,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC,qCAAqC,EAAE,CAAC;AAC5G,CAAC;AAyBD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,mCAAmC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,0FAA0F,EAAE,EAAE,CAAC;AAChN,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAwB,EAAE,GAAW,EAAE,WAAsC;IACxG,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,qGAAqG,EAAE,EAAE,CAAC;IAC7K,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE;YACL,KAAK,EAAE,cAAc,IAAI,6GAA6G;YACtI,IAAI,EAAE,0GAA0G;SACjH;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAe;IAC7D,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,+CAA+C;QAC1D,KAAK,EAAE;YACL,KAAK,EAAE,cAAc,IAAI,qBAAqB,SAAS,CAAC,IAAI,CAAC,mJAAmJ;YAChN,IAAI,EAAE,2GAA2G;SAClH;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAe,EAAE,IAAY;IACxE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAwB,EACxB,KAA+C,EAC/C,GAAW,EACX,WAAsC;IAEtC,IAAI,IAAiB,CAAC;IACtB,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAwE,CAAC;QACzF,OAAO,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnG,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;IACvD,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7H,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,IAAiB,EAAE,WAAsC;IAC/E,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACtC,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS,IAAI,YAAY,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9E,wEAAwE;IACxE,gDAAgD;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,GAAG,OAAO,oBAAoB,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;IAChG,iEAAiE;IACjE,sEAAsE;IACtE,gDAAgD;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAChF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/F,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,GAAG,OAAO,MAAM,IAAI,CAAC,MAAM,oBAAoB,MAAM,cAAc,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3I,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAClE,OAAO,GAAG,OAAO,UAAU,KAAK,CAAC,KAAK,CAAC,KAAK,SAAS,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5H,CAAC;IACD,yEAAyE;IACzE,sEAAsE;IACtE,kBAAkB;IAClB,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAC3B,IAAI,QAAQ,EAAE,MAAM,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACrG,OAAO,GAAG,OAAO,cAAc,QAAQ,CAAC,IAAI,2BAA2B,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC;IACjM,CAAC;IACD,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAA6B,EAAE,CAAC;QAClF,IAAI,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC;YAAE,SAAS;QAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,UAAU,CAAC,KAAK,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxD,OAAO,GAAG,OAAO,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,uBAAuB,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChI,CAAC;IACD,4EAA4E;IAC5E,uEAAuE;IACvE,yEAAyE;IACzE,oDAAoD;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvD,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,4EAA4E,CAAC;IAC/G,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\nimport { resolve } from 'node:path';\n\nimport { sameId } from './branch.js';\nimport { literal } from './literal.js';\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n /** Every value of each option that takes one, by its long name, in the order given; the fields below hold the last. */\n readonly written: Readonly<Record<string, readonly string[]>>;\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// Every value an option that takes one was given, so that an empty one is\n// refused wherever it stands: `--root \"\" --root .` names one directory twice\n// as the root is read, and the empty value is still the unset variable it\n// most often is. An option left out has no value to refuse, and a value that\n// names something is passed over as none is: the empty list is equivalent\n// to its mutant.\nconst every =\n (option: string) =>\n (line: CommandLine): readonly string[] =>\n line.written[option] ?? [];\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = literal({\n brief: { of: every('brief'), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: every('base'), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: every('root'), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: every('reason'), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: every('recommend'), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: every('show'), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: every('id'), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n}) satisfies Record<string, Valued>;\n\n/**\n * Whether two values of an option that takes one ask for one thing, as the\n * option reads them: a brief by its id as spec-brief's names are matched,\n * space around it dropped and `12` the brief `012` (ADR-0004); a root by the\n * directory it names from `cwd`, the one an empty root names on the hook's\n * line among them; any other as it is written, since each is read so, a\n * format, a probe's position, an id or text.\n */\nfunction asksOne(option: string, first: string, other: string, cwd: string): boolean {\n if (option === 'brief') return sameId(first.trim(), other.trim());\n if (option === 'root') return resolve(cwd, first) === resolve(cwd, other);\n return first === other;\n}\n\n/**\n * The first option that takes one value and was given two that read as two,\n * named with both as they were written; `null` where none was. The parser\n * keeps the last, and the earlier went unread with no word of it:\n * `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001`\n * was brief 001 (spec-core ADR-0005). The same value given twice, however it\n * is spelled, is the option once, and the last is the one read. `cwd` is\n * where a relative root is read from. A list, `--path` and `--option`, has\n * every value it is given.\n */\nexport function twoValues(line: Pick<CommandLine, 'written'>, cwd: string): string | null {\n for (const [option, values] of Object.entries(line.written)) {\n const first = values[0]!;\n const other = values.find((value) => !asksOne(option, first, value, cwd));\n if (other !== undefined) return `--${option} is given twice, as ${JSON.stringify(first)} and as ${JSON.stringify(other)}, and one of the two would go unread`;\n }\n return null;\n}\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });\nconst ONE_BRIEF = literal({\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n});\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = literal({\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n}) satisfies Record<string, Shape>;\n\n/** The options every command answers instead of running, which no shape lists. */\nconst ANSWERS: readonly string[] = ['help', 'version'];\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)!}`;\n}\n\n/**\n * Whether a line is Claude Code's hook, by its command and the argument\n * after it. It is exempt from every refusal here, and read as each release\n * since 0.1.0 read it: the line lives in settings files and in the plugin of\n * every release, which this one cannot rewrite, and exit 2 from it holds\n * every write of a session, the write that would fix the line among them.\n * A line the parser cannot read is asked the same, and the hook says it\n * with exit 1 (cli.ts).\n */\nexport function isClaudeHook(command: string | undefined, argument: string | undefined): boolean {\n return command === 'hook' && argument === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/** What is wrong with the line of Claude Code's hook, and what mends it. */\nexport interface LineFault {\n readonly wrong: string;\n readonly mend: string;\n}\n\n/**\n * What Claude Code's hook does with its own line, as far as the line decides\n * it: it guards, in the root the line gives it where it gives one and under\n * the brief it names where it names one; or the guard is off, for a fault\n * the hook says with exit 1 and holds no write for; or every write is held,\n * exit 2, for a brief the line names that is not one (ADR-0005). The hook\n * reads its line through the functions below, and `doctor` reads a line it\n * finds in Claude Code's settings through the same ones, so what it says of\n * a line is what the hook does with it.\n */\nexport type HookLine = Guards | GuardOff | WritesHeld;\n\nexport interface Guards {\n readonly state: 'guards';\n readonly root: string | undefined;\n /**\n * The brief the line names with `--brief`, as written. Whether it is a\n * brief is spec-brief's to say, which the caller asks. Absent where the\n * line names none.\n */\n readonly brief?: string;\n /**\n * The work tree the root is in, where that is not the project's own and\n * holds it: the hook opens the work tree its root is in, so a write to a\n * file of the project is checked there, by that work tree's briefs, under\n * the path the file has in it. Only git can tell, so the caller asks it.\n * Absent where the line gives no root, or one in the project's work tree.\n */\n readonly holder?: OtherTree & { readonly path: string };\n}\n\n/** A work tree the root on the hook's line is in that is not the project's own. */\nexport interface OtherTree {\n /** Its top, as git names it. */\n readonly top: string;\n /** Whether it is a work tree of the project's own repository, linked or main, where it is otherwise another repository's. */\n readonly ours: boolean;\n}\n\n/** How such a work tree is named: by its top, and whose it is. */\nexport function namedTree({ top, ours }: OtherTree): string {\n return `${top}, ${ours ? 'another work tree of this repository' : 'the work tree of another repository'}`;\n}\n\nexport interface GuardOff {\n readonly state: 'off';\n readonly fault: LineFault;\n /**\n * Set where the hook says nothing of the fault and exits 0, so that no\n * run of it fails: a root outside any work tree, or in a work tree that\n * does not hold the project. Every other fault it says, with exit 1.\n */\n readonly silent?: true;\n /**\n * What goes unchecked, where that is not every write: under a root in a\n * work tree that does not hold the project, the hook checks a write to a\n * file of that work tree, and none to a file of the project.\n */\n readonly unchecked?: string;\n}\n\nexport interface WritesHeld {\n readonly state: 'held';\n /** What is said of the name, by what named the brief (workspace.ts). */\n readonly wrong: string;\n}\n\n/**\n * A line under which the round has no brief to be checked against, since\n * what names one names a brief spec-brief does not know, or one it has\n * archived: the hook holds the write, exit 2, and every write after it until\n * the name is mended (ADR-0004). `wrong` is what is said of the name.\n */\nexport function heldHookLine(wrong: string): WritesHeld {\n return { state: 'held', wrong };\n}\n\n/**\n * A line the parser refused: an option no command has, an option left\n * without its value, a `--format` or an `--at` that is none, `--help` beside\n * `--version`; or a line it read with two values for an option that takes\n * one, of which it cannot tell the one meant ({@link twoValues}). The\n * parser's words are kept, on one line: what node says of an option that may\n * have lost its value is three.\n */\nexport function unreadHookLine(said: string): GuardOff {\n return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\\n').join(' ')})`, mend: 'mend it in Claude Code\\'s settings, where init writes \"hook claude\" and nothing after it' } };\n}\n\n/**\n * A line that parsed, by the root it gives. An empty root is the directory\n * the hook was started in: git starts there when it is handed no directory,\n * so the hook has guarded there for an empty root in every release. A root\n * that is a file, or is not there, is one git starts in neither of, which\n * read as a session outside a work tree, exit 0 and no word, and the guard\n * someone had configured was off.\n */\nexport function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff {\n const given = root === '' ? cwd : root;\n if (given !== undefined && !isDirectory(given)) {\n return { state: 'off', fault: { wrong: noDirectory(given), mend: \"give the hook's line a directory that exists, or leave --root out for the project Claude Code names\" } };\n }\n return { state: 'guards', root: given };\n}\n\n/**\n * A line whose root is a directory outside any git work tree. The hook\n * reads that as a session outside one, where it has nothing to guard: it\n * checks no write and says nothing, exit 0 (ADR-0005), and that stays. Only\n * git can tell such a root, so the caller asks it. A line that carries one\n * is a guard someone set up that checks nothing, and `doctor` says it as\n * off.\n */\nexport function outsideHookLine(root: string): GuardOff {\n return {\n state: 'off',\n silent: true,\n fault: {\n wrong: `--root is \"${root}\", a directory outside any git work tree, which the hook reads as a session outside one, exit 0 and no word`,\n mend: \"give the hook's line a directory in the work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line whose root is in a git work tree that does not hold the project:\n * another repository beside it, or a work tree inside it, a repository of\n * its own, a submodule or a linked worktree. The hook opens the work tree\n * its root is in and decides by that work tree's briefs. A file of the\n * project is outside it, which the hook reads as a file outside the\n * repository: it says nothing, exit 0, as it has in every release, and that\n * stays. So no write to a file of the project is checked under the line,\n * and `doctor` says it as a guard that is off. Only git can tell such a\n * root, so the caller asks it.\n */\nexport function elsewhereHookLine(root: string, tree: OtherTree): GuardOff {\n return {\n state: 'off',\n silent: true,\n unchecked: 'no write to a file of this project is checked',\n fault: {\n wrong: `--root is \"${root}\", a directory in ${namedTree(tree)}, which does not hold this project: the hook checks a write to a file of that work tree, by its briefs, and passes any other with no word, exit 0`,\n mend: \"give the hook's line a directory in this work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line that guards under a root in a work tree which holds the project\n * and is not its own: a repository the project is nested in, or the main\n * work tree around a linked one. `path` is the project's top as that work\n * tree names it.\n */\nexport function holderHookLine(line: Guards, tree: OtherTree, path: string): Guards {\n return { ...line, holder: { ...tree, path } };\n}\n\n/**\n * What Claude Code's hook does with the words it is handed, or `null` for\n * words that are not its line: read by the parser the command line reads\n * every line with, and known by its command and the argument after it on a\n * line that parsed and on one that did not, as the command line knows it\n * (cli.ts). `parse` is that parser, which throws its refusal with the words\n * it could still read.\n *\n * A line that guards says which brief it names, where it names one: the hook\n * passes over a `--brief` that is empty, as every release has, and holds\n * every write under one that names no brief, which only spec-brief can tell.\n * A line with two values for an option that takes one is a line the hook\n * cannot read, whichever option it is, read from `cwd` as the hook reads it\n * from where it is started (cli.ts).\n */\nexport function claudeHookLine(\n words: readonly string[],\n parse: (argv: readonly string[]) => CommandLine,\n cwd: string,\n isDirectory: (path: string) => boolean,\n): HookLine | null {\n let line: CommandLine;\n try {\n line = parse(words);\n } catch (error) {\n const refused = error as { readonly message: string; readonly words: readonly string[] };\n return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;\n }\n if (!isClaudeHook(line.command, line.positionals[0])) return null;\n const twice = twoValues(line, cwd);\n if (twice !== null) return unreadHookLine(twice);\n const read = readHookLine(line.root, cwd, isDirectory);\n return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known, and before\n * `--help` or `--version` answers: a line that is wrong is refused with\n * either on it, as one the parser refuses always was (spec-core ADR-0005).\n * A command with no shape is none, which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(command, positionals[0])) return null;\n // What the line asks the command, beside the help or the version it may\n // ask for instead, which every command answers.\n const asked = line.given.filter((option) => !ANSWERS.includes(option));\n const stray = asked.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => asked.includes(candidate.option));\n if (mode !== undefined) {\n const unread = asked.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\nimport { resolve } from 'node:path';\n\nimport { sameId } from './branch.js';\nimport { literal } from './literal.js';\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n /** Every value of each option that takes one, by its long name, in the order given; the fields below hold the last. */\n readonly written: Readonly<Record<string, readonly string[]>>;\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n /** Whether the line asks for the help, or for the version, which every command answers in place of running. */\n readonly help: boolean;\n readonly version: boolean;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// Every value an option that takes one was given, so that an empty one is\n// refused wherever it stands: `--root \"\" --root .` names one directory twice\n// as the root is read, and the empty value is still the unset variable it\n// most often is. An option left out has no value to refuse, and a value that\n// names something is passed over as none is: the empty list is equivalent\n// to its mutant.\nconst every =\n (option: string) =>\n (line: CommandLine): readonly string[] =>\n line.written[option] ?? [];\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = literal({\n brief: { of: every('brief'), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: every('base'), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: every('root'), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: every('reason'), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: every('recommend'), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: every('show'), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: every('id'), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n}) satisfies Record<string, Valued>;\n\n/**\n * A resolved path with its drive letter in one case. On Windows a drive\n * letter names its volume in either case, `D:\\` and `d:\\` alike (Microsoft's\n * Naming Files, Paths, and Namespaces, Naming Conventions, read 2026-10-19),\n * where Node's `resolve` keeps the case it was written in (spec-core\n * ADR-0005, amended 2026-10-19). The rest of the path is left as it is: NTFS\n * can be made to tell names apart by case, so whether two names are one\n * directory is the disk's to say. A path resolved elsewhere starts with `/`,\n * and has no drive letter to fold.\n */\nexport function foldDrive(path: string): string {\n return /^[a-z]:/.test(path) ? `${path[0]!.toUpperCase()}${path.slice(1)}` : path;\n}\n\n/**\n * Whether two values of an option that takes one ask for one thing, as the\n * option reads them: a brief by its id as spec-brief's names are matched,\n * space around it dropped and `12` the brief `012` (ADR-0004); a root by the\n * directory it names from `cwd`, the one an empty root names on the hook's\n * line among them, with a drive letter in either case; any other as it is\n * written, since each is read so, a format, a probe's position, an id or\n * text.\n */\nfunction asksOne(option: string, first: string, other: string, cwd: string): boolean {\n if (option === 'brief') return sameId(first.trim(), other.trim());\n if (option === 'root') return foldDrive(resolve(cwd, first)) === foldDrive(resolve(cwd, other));\n return first === other;\n}\n\n/**\n * The first option that takes one value and was given two that read as two,\n * named with both as they were written; `null` where none was. The parser\n * keeps the last, and the earlier went unread with no word of it:\n * `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001`\n * was brief 001 (spec-core ADR-0005). The same value given twice, however it\n * is spelled, is the option once, and the last is the one read. `cwd` is\n * where a relative root is read from. A list, `--path` and `--option`, has\n * every value it is given.\n */\nexport function twoValues(line: Pick<CommandLine, 'written'>, cwd: string): string | null {\n for (const [option, values] of Object.entries(line.written)) {\n const first = values[0]!;\n const other = values.find((value) => !asksOne(option, first, value, cwd));\n if (other !== undefined) return `--${option} is given twice, as ${JSON.stringify(first)} and as ${JSON.stringify(other)}, and one of the two would go unread`;\n }\n return null;\n}\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });\nconst ONE_BRIEF = literal({\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n});\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = literal({\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook', 'block-on-failure'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n}) satisfies Record<string, Shape>;\n\n/** The options every command answers instead of running, which no shape lists. */\nconst ANSWERS: readonly string[] = ['help', 'version'];\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)!}`;\n}\n\n/**\n * Whether a line is Claude Code's hook, by its command and the argument\n * after it. It is exempt from every refusal here, and read as each release\n * since 0.1.0 read it: the line lives in settings files and in the plugin of\n * every release, which this one cannot rewrite, and exit 2 from it holds\n * every write of a session, the write that would fix the line among them.\n * A line the parser cannot read is asked the same, and the hook says it\n * to the person with exit 0 (cli.ts).\n */\nexport function isClaudeHook(command: string | undefined, argument: string | undefined): boolean {\n return command === 'hook' && argument === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/** What is wrong with the line of Claude Code's hook, and what mends it. */\nexport interface LineFault {\n readonly wrong: string;\n readonly mend: string;\n}\n\n/**\n * What Claude Code's hook does with its own line, as far as the line decides\n * it: it guards, in the root the line gives it where it gives one and under\n * the brief it names where it names one; or the guard is off, for a fault\n * the hook says with exit 0 and holds no write for; or every write is held,\n * exit 2, for a brief the line names that is not one (ADR-0005). The hook\n * reads its line through the functions below, and `doctor` reads a line it\n * finds in Claude Code's settings through the same ones, so what it says of\n * a line is what the hook does with it.\n */\nexport type HookLine = Guards | GuardOff | WritesHeld;\n\nexport interface Guards {\n readonly state: 'guards';\n readonly root: string | undefined;\n /**\n * The brief the line names with `--brief`, as written. Whether it is a\n * brief is spec-brief's to say, which the caller asks. Absent where the\n * line names none.\n */\n readonly brief?: string;\n /**\n * The work tree the root is in, where that is not the project's own and\n * holds it: the hook opens the work tree its root is in, so a write to a\n * file of the project is checked there, by that work tree's briefs, under\n * the path the file has in it. Only git can tell, so the caller asks it.\n * Absent where the line gives no root, or one in the project's work tree.\n */\n readonly holder?: OtherTree & { readonly path: string };\n}\n\n/** A work tree the root on the hook's line is in that is not the project's own. */\nexport interface OtherTree {\n /** Its top, as git names it. */\n readonly top: string;\n /** Whether it is a work tree of the project's own repository, linked or main, where it is otherwise another repository's. */\n readonly ours: boolean;\n}\n\n/** How such a work tree is named: by its top, and whose it is. */\nexport function namedTree({ top, ours }: OtherTree): string {\n return `${top}, ${ours ? 'another work tree of this repository' : 'the work tree of another repository'}`;\n}\n\nexport interface GuardOff {\n readonly state: 'off';\n readonly fault: LineFault;\n /**\n * Set where the hook says nothing of the fault: a root outside any work\n * tree, or in a work tree that does not hold the project. Every other\n * fault it says, to the person, and every one with exit 0.\n */\n readonly silent?: true;\n /**\n * What goes unchecked, where that is not every write: under a root in a\n * work tree that does not hold the project, the hook checks a write to a\n * file of that work tree, and none to a file of the project.\n */\n readonly unchecked?: string;\n}\n\nexport interface WritesHeld {\n readonly state: 'held';\n /** What is said of the name, by what named the brief (workspace.ts). */\n readonly wrong: string;\n}\n\n/**\n * A line under which the round has no brief to be checked against, since\n * what names one names a brief spec-brief does not know, or one it has\n * archived: the hook holds the write, exit 2, and every write after it until\n * the name is mended (ADR-0004). `wrong` is what is said of the name.\n */\nexport function heldHookLine(wrong: string): WritesHeld {\n return { state: 'held', wrong };\n}\n\n/**\n * A line the parser refused: an option no command has, an option left\n * without its value, a `--format` or an `--at` that is none, `--help` beside\n * `--version`; or a line it read with two values for an option that takes\n * one, of which it cannot tell the one meant ({@link twoValues}). The\n * parser's words are kept, on one line: what node says of an option that may\n * have lost its value is three.\n */\nexport function unreadHookLine(said: string): GuardOff {\n return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\\n').join(' ')})`, mend: 'mend it in Claude Code\\'s settings, where init writes \"hook claude\" and nothing after it' } };\n}\n\n/**\n * A line that parsed, by the root it gives. An empty root is the directory\n * the hook was started in: git starts there when it is handed no directory,\n * so the hook has guarded there for an empty root in every release. A root\n * that is a file, or is not there, is one git starts in neither of, which\n * read as a session outside a work tree, exit 0 and no word, and the guard\n * someone had configured was off.\n */\nexport function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff {\n const given = root === '' ? cwd : root;\n if (given !== undefined && !isDirectory(given)) {\n return { state: 'off', fault: { wrong: noDirectory(given), mend: \"give the hook's line a directory that exists, or leave --root out for the project Claude Code names\" } };\n }\n return { state: 'guards', root: given };\n}\n\n/**\n * What `--help` or `--version` on the hook's line stands beside that the\n * hook would say as it runs, in the words for a line it cannot read; `null`\n * where the line asks for neither, or stands beside nothing the line itself\n * shows to be wrong. Every command answers either before it runs, so the\n * hook printed the help over a root that is not a directory, exit 0, and\n * said nothing of it (ADR-0005). A fault only git or spec-brief can tell\n * stays unsaid: neither is asked before an answer that runs nothing.\n */\nexport function answerBeside(line: Pick<CommandLine, 'help' | 'version' | 'root'>, cwd: string, isDirectory: (path: string) => boolean): string | null {\n if (!line.help && !line.version) return null;\n const read = readHookLine(line.root, cwd, isDirectory);\n if (read.state === 'guards') return null;\n const asked = line.help ? 'help' : 'version';\n return `--${asked} is given beside a fault, ${read.fault.wrong}, and the ${asked} would be printed in its place`;\n}\n\n/**\n * A line of the hook's that asks for the help or the version, which the hook\n * prints, exit 0, in place of reading its question: no write is checked\n * under it, and Claude Code, by its hooks reference, writes what a hook\n * that exits 0 prints to its debug log, so nothing says the guard is off\n * (ADR-0005). The hook says nothing of it, since a person who asks the hook\n * for its help at a terminal is answered the same way.\n */\nexport function answeringHookLine(asked: 'help' | 'version'): GuardOff {\n return {\n state: 'off',\n silent: true,\n fault: {\n wrong: `the line asks for the ${asked}, which the hook prints, exit 0, in place of checking the write`,\n mend: 'take that option off the line, where init writes \"hook claude\" and nothing after it',\n },\n };\n}\n\n/**\n * A line whose root is a directory outside any git work tree. The hook\n * reads that as a session outside one, where it has nothing to guard: it\n * checks no write and says nothing, exit 0 (ADR-0005), and that stays. Only\n * git can tell such a root, so the caller asks it. A line that carries one\n * is a guard someone set up that checks nothing, and `doctor` says it as\n * off.\n */\nexport function outsideHookLine(root: string): GuardOff {\n return {\n state: 'off',\n silent: true,\n fault: {\n wrong: `--root is \"${root}\", a directory outside any git work tree, which the hook reads as a session outside one, exit 0 and no word`,\n mend: \"give the hook's line a directory in the work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line whose root is in a git work tree that does not hold the project:\n * another repository beside it, or a work tree inside it, a repository of\n * its own, a submodule or a linked worktree. The hook opens the work tree\n * its root is in and decides by that work tree's briefs. A file of the\n * project is outside it, which the hook reads as a file outside the\n * repository: it says nothing, exit 0, as it has in every release, and that\n * stays. So no write to a file of the project is checked under the line,\n * and `doctor` says it as a guard that is off. Only git can tell such a\n * root, so the caller asks it.\n */\nexport function elsewhereHookLine(root: string, tree: OtherTree): GuardOff {\n return {\n state: 'off',\n silent: true,\n unchecked: 'no write to a file of this project is checked',\n fault: {\n wrong: `--root is \"${root}\", a directory in ${namedTree(tree)}, which does not hold this project: the hook checks a write to a file of that work tree, by its briefs, and passes any other with no word, exit 0`,\n mend: \"give the hook's line a directory in this work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line that guards under a root in a work tree which holds the project\n * and is not its own: a repository the project is nested in, or the main\n * work tree around a linked one. `path` is the project's top as that work\n * tree names it.\n */\nexport function holderHookLine(line: Guards, tree: OtherTree, path: string): Guards {\n return { ...line, holder: { ...tree, path } };\n}\n\n/**\n * What Claude Code's hook does with the words it is handed, or `null` for\n * words that are not its line: read by the parser the command line reads\n * every line with, and known by its command and the argument after it on a\n * line that parsed and on one that did not, as the command line knows it\n * (cli.ts). `parse` is that parser, which throws its refusal with the words\n * it could still read.\n *\n * A line that guards says which brief it names, where it names one: the hook\n * passes over a `--brief` that is empty, as every release has, and holds\n * every write under one that names no brief, which only spec-brief can tell.\n * A line with two values for an option that takes one is a line the hook\n * cannot read, whichever option it is, read from `cwd` as the hook reads it\n * from where it is started (cli.ts). So is one with the help or the version\n * beside a root that is not a directory; and with either beside nothing it\n * can see is wrong, the line checks no write, whatever else it holds.\n */\nexport function claudeHookLine(\n words: readonly string[],\n parse: (argv: readonly string[]) => CommandLine,\n cwd: string,\n isDirectory: (path: string) => boolean,\n): HookLine | null {\n let line: CommandLine;\n try {\n line = parse(words);\n } catch (error) {\n const refused = error as { readonly message: string; readonly words: readonly string[] };\n return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;\n }\n if (!isClaudeHook(line.command, line.positionals[0])) return null;\n const twice = twoValues(line, cwd);\n if (twice !== null) return unreadHookLine(twice);\n const beside = answerBeside(line, cwd, isDirectory);\n if (beside !== null) return unreadHookLine(beside);\n if (line.help) return answeringHookLine('help');\n if (line.version) return answeringHookLine('version');\n const read = readHookLine(line.root, cwd, isDirectory);\n return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known, and before\n * `--help` or `--version` answers: a line that is wrong is refused with\n * either on it, as one the parser refuses always was (spec-core ADR-0005).\n * A command with no shape is none, which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(command, positionals[0])) return null;\n // What the line asks the command, beside the help or the version it may\n // ask for instead, which every command answers.\n const asked = line.given.filter((option) => !ANSWERS.includes(option));\n const stray = asked.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => asked.includes(candidate.option));\n if (mode !== undefined) {\n const unread = asked.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
@@ -60,6 +60,8 @@ export interface Options {
60
60
  readonly write: boolean;
61
61
  /** init: also install git's pre-commit hook. */
62
62
  readonly gitHook: boolean;
63
+ /** init: `onFailure: "block"` on the hook entries it writes (ADR-0012). */
64
+ readonly blockOnFailure: boolean;
63
65
  /** escalate: the files the round needs, the reason, the options and the recommendation. */
64
66
  readonly paths: readonly string[];
65
67
  readonly reason: string | undefined;
@@ -112,9 +114,10 @@ export declare function openWorkspace(options: Pick<Options, 'root'>, io: CliIO,
112
114
  /** The installs a work tree's siblings are looked for in after its own, given its top and its common directory (siblings.ts). */
113
115
  export type Installs = (root: string, commonDir: string) => readonly string[];
114
116
  /**
115
- * git's hook and doctor: a linked worktree with no install of its own is
116
- * checked with the main work tree's, where `git worktree add` leaves the
117
- * project's `node_modules` (ADR-0005).
117
+ * Every command: a linked worktree with no install of its own is checked
118
+ * with the main work tree's, where `git worktree add` leaves the project's
119
+ * `node_modules`, as git's hook and doctor were, and as the hook is with the
120
+ * project's (ADR-0005).
118
121
  */
119
122
  export declare const MAIN_INSTALL: Installs;
120
123
  /** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */
@@ -155,3 +158,42 @@ export declare function describeActive(active: ActiveBrief, argument?: string):
155
158
  problem: string;
156
159
  };
157
160
  export declare function json(command: string, body: Record<string, unknown>): string;
161
+ /**
162
+ * Whether a work tree other than the one whose top is `top` may hold `cwd`:
163
+ * `cwd` is outside it, or a `.git` stands in a directory between the two,
164
+ * where a linked worktree, a submodule or a repository of its own starts.
165
+ * Where neither is so, `cwd` is in that work tree, and git is asked nothing
166
+ * more: a session works at its project's top, or below it, for nearly every
167
+ * write. Answering that one may is what git is then asked, so the mutant
168
+ * that always answers so is equivalent, at the cost of a process, and so
169
+ * is each that only answers so more often: a slice one short reaches the
170
+ * top itself, which holds a `.git`, and a part named in place of none is
171
+ * a directory no top holds.
172
+ */
173
+ export declare function mayBeElsewhere(cwd: string, top: string): boolean;
174
+ /**
175
+ * The work tree that holds `cwd`, where it is another work tree of the
176
+ * repository the work tree `named` is one of, as the hook's root names it, by the git directory the two share;
177
+ * `null` where `cwd` is in the root's own, in another repository, or in
178
+ * none (ADR-0005).
179
+ */
180
+ export declare function otherTree(named: Workspace, cwd: string): Promise<FoundTree | null>;
181
+ /**
182
+ * The work trees a write is decided in, each with the paths it decides, for
183
+ * the hook and for the server's `check_path` (ADR-0005). The repository is
184
+ * the one the hook's root names, as before. Each path is decided by the
185
+ * innermost work tree of that repository that holds it, by its branch, its
186
+ * briefs and its configuration, read from its top, with its siblings looked
187
+ * for in its install and then the project's: there a session works in a
188
+ * worktree while Claude Code names the project it started in, or writes from
189
+ * one into another, and the hook decided by the project's branch, which
190
+ * named no brief, so a protected write passed with no word. A path in no
191
+ * other work tree of the repository is decided by the work tree the root
192
+ * names, as before: a write to the project's own file from a worktree is
193
+ * refused where it was, and a path in another repository, or in none, is
194
+ * read as it was. `cwd` is what a relative path is read from.
195
+ */
196
+ export declare function writesBy(named: Workspace, cwd: string | null, paths: readonly string[], io: CliIO): Promise<{
197
+ readonly workspace: Workspace;
198
+ readonly paths: readonly string[];
199
+ }[]>;