@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +212 -0
- package/README.md +30 -15
- package/dist/briefs.d.ts +45 -0
- package/dist/briefs.js +73 -0
- package/dist/briefs.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +211 -134
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +7 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +14 -3
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +55 -1
- package/dist/configure.js +130 -22
- package/dist/configure.js.map +1 -1
- package/dist/fs.d.ts +78 -1
- package/dist/fs.js +289 -10
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +50 -2
- package/dist/git.js +98 -8
- package/dist/git.js.map +1 -1
- package/dist/hooks.d.ts +45 -0
- package/dist/hooks.js +62 -3
- package/dist/hooks.js.map +1 -1
- package/dist/round.d.ts +8 -0
- package/dist/round.js +23 -8
- package/dist/round.js.map +1 -1
- package/dist/sandbox.js +4 -3
- package/dist/sandbox.js.map +1 -1
- package/dist/server.d.ts +25 -1
- package/dist/server.js +194 -27
- package/dist/server.js.map +1 -1
- package/dist/setup.js +40 -10
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +24 -3
- package/dist/siblings.js +106 -14
- package/dist/siblings.js.map +1 -1
- package/dist/usage.d.ts +42 -7
- package/dist/usage.js +65 -6
- package/dist/usage.js.map +1 -1
- package/dist/workspace.d.ts +45 -3
- package/dist/workspace.js +133 -13
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -33,11 +33,19 @@ export const DEFAULT_CONFIG = {
|
|
|
33
33
|
assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },
|
|
34
34
|
probes: { runs: 2, timeout: 600 },
|
|
35
35
|
tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },
|
|
36
|
+
turnReport: false,
|
|
36
37
|
};
|
|
37
38
|
export class ConfigError extends Error {
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The configuration file that did not load, where one stands at its path:
|
|
41
|
+
* the one file a write may still reach while it does not (ADR-0005).
|
|
42
|
+
* `null` for a configuration that was not read from a file.
|
|
43
|
+
*/
|
|
44
|
+
file;
|
|
45
|
+
constructor(message, file = null) {
|
|
39
46
|
super(message);
|
|
40
47
|
this.name = 'ConfigError';
|
|
48
|
+
this.file = file;
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
function isObject(value) {
|
|
@@ -68,7 +76,7 @@ function nonEmpty(value, where) {
|
|
|
68
76
|
export function parseConfig(raw, file = CONFIG_FILE) {
|
|
69
77
|
if (!isObject(raw))
|
|
70
78
|
throw new ConfigError(`${file} must hold a JSON object`);
|
|
71
|
-
checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);
|
|
79
|
+
checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools', 'turnReport'], file);
|
|
72
80
|
let branches = DEFAULT_CONFIG.branches;
|
|
73
81
|
if (raw['branches'] !== undefined) {
|
|
74
82
|
if (!isStringList(raw['branches']) || raw['branches'].length === 0) {
|
|
@@ -168,6 +176,9 @@ export function parseConfig(raw, file = CONFIG_FILE) {
|
|
|
168
176
|
tools[name] = command;
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
|
-
|
|
179
|
+
const turnReport = raw['turnReport'] ?? DEFAULT_CONFIG.turnReport;
|
|
180
|
+
if (typeof turnReport !== 'boolean')
|
|
181
|
+
throw new ConfigError(`${file}: "turnReport" must be true or false`);
|
|
182
|
+
return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools, turnReport };
|
|
172
183
|
}
|
|
173
184
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `.spec-harness.json` may say, the defaults, and validation.\n *\n * Every key has a default, so a repository without the file works; a file\n * that says something this version does not understand stops the run with\n * exit 2, because a guard that silently ignored a misspelt key would be\n * guarding something other than what its owner wrote.\n */\n\nimport { templateError } from './branch.js';\n\nexport type OutOfScope = 'warn' | 'ask' | 'deny';\n\nexport interface HarnessConfig {\n /** Branch-name templates that carry the active brief's id (ADR-0004). */\n readonly branches: readonly string[];\n /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */\n readonly outOfScope: OutOfScope;\n /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */\n readonly base: string | null;\n readonly rulings: {\n /** The brief section whose table holds the rulings. */\n readonly section: string;\n /** The allowed-signers file, read from the base branch, never from the round's own tree. */\n readonly allowedSigners: string;\n };\n readonly dependencies: {\n /** Manifest file names whose added dependencies an audit reports. */\n readonly manifests: readonly string[];\n };\n readonly context: {\n /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */\n readonly budget: number;\n };\n readonly assertions: {\n /**\n * Brief sections whose assertions state what was true before the round -\n * the defect, measured - and are expected to stop holding when it ends.\n * Every other assertion in a brief is a goal.\n */\n readonly premises: readonly string[];\n };\n readonly probes: {\n /** How many times a probe runs; every run must agree. */\n readonly runs: number;\n /** Seconds one run may take. */\n readonly timeout: number;\n };\n /** The command for each sibling tool, or `null` to find it (ADR-0002). */\n readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;\n}\n\nexport type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';\nexport const SIBLINGS: readonly SiblingName[] = ['spec-brief', 'spec-graph', 'spec-guard'];\n\nexport const CONFIG_FILE = '.spec-harness.json';\n\n/** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */\nexport const SPEC_BRIEF_CONFIGS: readonly string[] = ['.spec-brief.json', 'spec-brief.json'];\n\nexport const DEFAULT_MANIFESTS: readonly string[] = [\n 'package.json',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'requirements*.txt',\n 'Directory.Packages.props',\n '*.csproj',\n '*.fsproj',\n '*.vbproj',\n 'Gemfile',\n];\n\nexport const DEFAULT_CONFIG: HarnessConfig = {\n branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],\n outOfScope: 'warn',\n base: null,\n rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },\n dependencies: { manifests: DEFAULT_MANIFESTS },\n context: { budget: 60_000 },\n assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },\n probes: { runs: 2, timeout: 600 },\n tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },\n};\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringList(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\nfunction checkKeys(value: Json, allowed: readonly string[], where: string): void {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n throw new ConfigError(`${where}: unknown key \"${key}\"; the keys are ${allowed.join(', ')}`);\n }\n }\n}\n\nfunction positive(value: unknown, where: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new ConfigError(`${where} must be a whole number of at least 1`);\n }\n return value;\n}\n\nfunction nonEmpty(value: unknown, where: string): string {\n if (typeof value !== 'string' || value.trim() === '') throw new ConfigError(`${where} must be a non-empty string`);\n return value;\n}\n\n/** Validates a parsed configuration and fills in every default. */\nexport function parseConfig(raw: unknown, file = CONFIG_FILE): HarnessConfig {\n if (!isObject(raw)) throw new ConfigError(`${file} must hold a JSON object`);\n checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);\n\n let branches = DEFAULT_CONFIG.branches;\n if (raw['branches'] !== undefined) {\n if (!isStringList(raw['branches']) || raw['branches'].length === 0) {\n throw new ConfigError(`${file}: \"branches\" must be a non-empty list of templates`);\n }\n for (const template of raw['branches']) {\n const error = templateError(template);\n if (error !== null) throw new ConfigError(`${file}: \"branches\": ${error}`);\n }\n branches = raw['branches'];\n }\n\n let outOfScope = DEFAULT_CONFIG.outOfScope;\n if (raw['outOfScope'] !== undefined) {\n if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {\n throw new ConfigError(`${file}: \"outOfScope\" must be \"warn\", \"ask\" or \"deny\"`);\n }\n outOfScope = raw['outOfScope'];\n }\n\n let base = DEFAULT_CONFIG.base;\n if (raw['base'] !== undefined && raw['base'] !== null) base = nonEmpty(raw['base'], `${file}: \"base\"`);\n\n let rulings = DEFAULT_CONFIG.rulings;\n if (raw['rulings'] !== undefined) {\n const value = raw['rulings'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"rulings\" must be an object`);\n checkKeys(value, ['section', 'allowedSigners'], `${file}: \"rulings\"`);\n rulings = {\n section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: \"rulings.section\"`),\n allowedSigners:\n value['allowedSigners'] === undefined\n ? rulings.allowedSigners\n : nonEmpty(value['allowedSigners'], `${file}: \"rulings.allowedSigners\"`),\n };\n }\n\n let dependencies = DEFAULT_CONFIG.dependencies;\n if (raw['dependencies'] !== undefined) {\n const value = raw['dependencies'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"dependencies\" must be an object`);\n checkKeys(value, ['manifests'], `${file}: \"dependencies\"`);\n if (value['manifests'] !== undefined) {\n if (!isStringList(value['manifests'])) throw new ConfigError(`${file}: \"dependencies.manifests\" must be a list of file names`);\n dependencies = { manifests: value['manifests'] };\n }\n }\n\n let context = DEFAULT_CONFIG.context;\n if (raw['context'] !== undefined) {\n const value = raw['context'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"context\" must be an object`);\n checkKeys(value, ['budget'], `${file}: \"context\"`);\n if (value['budget'] !== undefined) context = { budget: positive(value['budget'], `${file}: \"context.budget\"`) };\n }\n\n let assertions = DEFAULT_CONFIG.assertions;\n if (raw['assertions'] !== undefined) {\n const value = raw['assertions'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"assertions\" must be an object`);\n checkKeys(value, ['premises'], `${file}: \"assertions\"`);\n if (value['premises'] !== undefined) {\n if (!isStringList(value['premises'])) throw new ConfigError(`${file}: \"assertions.premises\" must be a list of section names`);\n assertions = { premises: value['premises'] };\n }\n }\n\n let probes = DEFAULT_CONFIG.probes;\n if (raw['probes'] !== undefined) {\n const value = raw['probes'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"probes\" must be an object`);\n checkKeys(value, ['runs', 'timeout'], `${file}: \"probes\"`);\n probes = {\n runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: \"probes.runs\"`),\n timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: \"probes.timeout\"`),\n };\n }\n\n const tools: Record<SiblingName, readonly string[] | null> = { ...DEFAULT_CONFIG.tools };\n if (raw['tools'] !== undefined) {\n const value = raw['tools'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"tools\" must be an object`);\n checkKeys(value, SIBLINGS, `${file}: \"tools\"`);\n for (const name of SIBLINGS) {\n const command = value[name];\n if (command === undefined || command === null) continue;\n // A list that is not empty has a first entry: the `?.` is the\n // compiler's, and the mutant without it is equivalent.\n if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {\n // A sibling runs without a shell, so the example is node and the\n // script: npx is a shim Windows cannot start without one.\n throw new ConfigError(`${file}: \"tools.${name}\" must be a command as a list, such as [\"node\", \"node_modules/@descent-vtt/${name}/bin/${name}.js\"], or null`);\n }\n tools[name] = command;\n }\n }\n\n return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `.spec-harness.json` may say, the defaults, and validation.\n *\n * Every key has a default, so a repository without the file works; a file\n * that says something this version does not understand stops the run with\n * exit 2, because a guard that silently ignored a misspelt key would be\n * guarding something other than what its owner wrote.\n */\n\nimport { templateError } from './branch.js';\n\nexport type OutOfScope = 'warn' | 'ask' | 'deny';\n\nexport interface HarnessConfig {\n /** Branch-name templates that carry the active brief's id (ADR-0004). */\n readonly branches: readonly string[];\n /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */\n readonly outOfScope: OutOfScope;\n /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */\n readonly base: string | null;\n readonly rulings: {\n /** The brief section whose table holds the rulings. */\n readonly section: string;\n /** The allowed-signers file, read from the base branch, never from the round's own tree. */\n readonly allowedSigners: string;\n };\n readonly dependencies: {\n /** Manifest file names whose added dependencies an audit reports. */\n readonly manifests: readonly string[];\n };\n readonly context: {\n /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */\n readonly budget: number;\n };\n readonly assertions: {\n /**\n * Brief sections whose assertions state what was true before the round -\n * the defect, measured - and are expected to stop holding when it ends.\n * Every other assertion in a brief is a goal.\n */\n readonly premises: readonly string[];\n };\n readonly probes: {\n /** How many times a probe runs; every run must agree. */\n readonly runs: number;\n /** Seconds one run may take. */\n readonly timeout: number;\n };\n /** The command for each sibling tool, or `null` to find it (ADR-0002). */\n readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;\n /**\n * Whether Claude Code's hook reports, at the end of each turn, the changed\n * paths the brief protects or that are outside its scope; off by default,\n * and `init` writes the entry that runs it only where it is on (ADR-0005).\n */\n readonly turnReport: boolean;\n}\n\nexport type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';\nexport const SIBLINGS: readonly SiblingName[] = ['spec-brief', 'spec-graph', 'spec-guard'];\n\nexport const CONFIG_FILE = '.spec-harness.json';\n\n/** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */\nexport const SPEC_BRIEF_CONFIGS: readonly string[] = ['.spec-brief.json', 'spec-brief.json'];\n\nexport const DEFAULT_MANIFESTS: readonly string[] = [\n 'package.json',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'requirements*.txt',\n 'Directory.Packages.props',\n '*.csproj',\n '*.fsproj',\n '*.vbproj',\n 'Gemfile',\n];\n\nexport const DEFAULT_CONFIG: HarnessConfig = {\n branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],\n outOfScope: 'warn',\n base: null,\n rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },\n dependencies: { manifests: DEFAULT_MANIFESTS },\n context: { budget: 60_000 },\n assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },\n probes: { runs: 2, timeout: 600 },\n tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },\n turnReport: false,\n};\n\nexport class ConfigError extends Error {\n /**\n * The configuration file that did not load, where one stands at its path:\n * the one file a write may still reach while it does not (ADR-0005).\n * `null` for a configuration that was not read from a file.\n */\n readonly file: string | null;\n\n constructor(message: string, file: string | null = null) {\n super(message);\n this.name = 'ConfigError';\n this.file = file;\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringList(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\nfunction checkKeys(value: Json, allowed: readonly string[], where: string): void {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n throw new ConfigError(`${where}: unknown key \"${key}\"; the keys are ${allowed.join(', ')}`);\n }\n }\n}\n\nfunction positive(value: unknown, where: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new ConfigError(`${where} must be a whole number of at least 1`);\n }\n return value;\n}\n\nfunction nonEmpty(value: unknown, where: string): string {\n if (typeof value !== 'string' || value.trim() === '') throw new ConfigError(`${where} must be a non-empty string`);\n return value;\n}\n\n/** Validates a parsed configuration and fills in every default. */\nexport function parseConfig(raw: unknown, file = CONFIG_FILE): HarnessConfig {\n if (!isObject(raw)) throw new ConfigError(`${file} must hold a JSON object`);\n checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools', 'turnReport'], file);\n\n let branches = DEFAULT_CONFIG.branches;\n if (raw['branches'] !== undefined) {\n if (!isStringList(raw['branches']) || raw['branches'].length === 0) {\n throw new ConfigError(`${file}: \"branches\" must be a non-empty list of templates`);\n }\n for (const template of raw['branches']) {\n const error = templateError(template);\n if (error !== null) throw new ConfigError(`${file}: \"branches\": ${error}`);\n }\n branches = raw['branches'];\n }\n\n let outOfScope = DEFAULT_CONFIG.outOfScope;\n if (raw['outOfScope'] !== undefined) {\n if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {\n throw new ConfigError(`${file}: \"outOfScope\" must be \"warn\", \"ask\" or \"deny\"`);\n }\n outOfScope = raw['outOfScope'];\n }\n\n let base = DEFAULT_CONFIG.base;\n if (raw['base'] !== undefined && raw['base'] !== null) base = nonEmpty(raw['base'], `${file}: \"base\"`);\n\n let rulings = DEFAULT_CONFIG.rulings;\n if (raw['rulings'] !== undefined) {\n const value = raw['rulings'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"rulings\" must be an object`);\n checkKeys(value, ['section', 'allowedSigners'], `${file}: \"rulings\"`);\n rulings = {\n section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: \"rulings.section\"`),\n allowedSigners:\n value['allowedSigners'] === undefined\n ? rulings.allowedSigners\n : nonEmpty(value['allowedSigners'], `${file}: \"rulings.allowedSigners\"`),\n };\n }\n\n let dependencies = DEFAULT_CONFIG.dependencies;\n if (raw['dependencies'] !== undefined) {\n const value = raw['dependencies'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"dependencies\" must be an object`);\n checkKeys(value, ['manifests'], `${file}: \"dependencies\"`);\n if (value['manifests'] !== undefined) {\n if (!isStringList(value['manifests'])) throw new ConfigError(`${file}: \"dependencies.manifests\" must be a list of file names`);\n dependencies = { manifests: value['manifests'] };\n }\n }\n\n let context = DEFAULT_CONFIG.context;\n if (raw['context'] !== undefined) {\n const value = raw['context'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"context\" must be an object`);\n checkKeys(value, ['budget'], `${file}: \"context\"`);\n if (value['budget'] !== undefined) context = { budget: positive(value['budget'], `${file}: \"context.budget\"`) };\n }\n\n let assertions = DEFAULT_CONFIG.assertions;\n if (raw['assertions'] !== undefined) {\n const value = raw['assertions'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"assertions\" must be an object`);\n checkKeys(value, ['premises'], `${file}: \"assertions\"`);\n if (value['premises'] !== undefined) {\n if (!isStringList(value['premises'])) throw new ConfigError(`${file}: \"assertions.premises\" must be a list of section names`);\n assertions = { premises: value['premises'] };\n }\n }\n\n let probes = DEFAULT_CONFIG.probes;\n if (raw['probes'] !== undefined) {\n const value = raw['probes'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"probes\" must be an object`);\n checkKeys(value, ['runs', 'timeout'], `${file}: \"probes\"`);\n probes = {\n runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: \"probes.runs\"`),\n timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: \"probes.timeout\"`),\n };\n }\n\n const tools: Record<SiblingName, readonly string[] | null> = { ...DEFAULT_CONFIG.tools };\n if (raw['tools'] !== undefined) {\n const value = raw['tools'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"tools\" must be an object`);\n checkKeys(value, SIBLINGS, `${file}: \"tools\"`);\n for (const name of SIBLINGS) {\n const command = value[name];\n if (command === undefined || command === null) continue;\n // A list that is not empty has a first entry: the `?.` is the\n // compiler's, and the mutant without it is equivalent.\n if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {\n // A sibling runs without a shell, so the example is node and the\n // script: npx is a shim Windows cannot start without one.\n throw new ConfigError(`${file}: \"tools.${name}\" must be a command as a list, such as [\"node\", \"node_modules/@descent-vtt/${name}/bin/${name}.js\"], or null`);\n }\n tools[name] = command;\n }\n }\n\n const turnReport = raw['turnReport'] ?? DEFAULT_CONFIG.turnReport;\n if (typeof turnReport !== 'boolean') throw new ConfigError(`${file}: \"turnReport\" must be true or false`);\n\n return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools, turnReport };\n}\n"]}
|
package/dist/configure.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export declare const PROJECT_DIR_OR_HERE = "${CLAUDE_PROJECT_DIR:-.}";
|
|
|
25
25
|
* path is one argument whatever it holds, and no shell is involved.
|
|
26
26
|
*/
|
|
27
27
|
export declare const GUARD_HOOK: Readonly<Json>;
|
|
28
|
+
/**
|
|
29
|
+
* The hook's entry at the end of a turn: the guard's line, run in the
|
|
30
|
+
* background. Claude Code waits on no part of it, so the turn ends as it
|
|
31
|
+
* would, and it hands the hook's `systemMessage` to Claude as the next
|
|
32
|
+
* turn begins, where from an entry it waits on it shows the person the
|
|
33
|
+
* message instead (the hooks reference,
|
|
34
|
+
* https://code.claude.com/docs/en/hooks, *Run hooks in the background* and
|
|
35
|
+
* *JSON output*, read 2026-10-19).
|
|
36
|
+
*/
|
|
37
|
+
export declare const TURN_HOOK: Readonly<Json>;
|
|
28
38
|
/**
|
|
29
39
|
* The release of Claude Code from which a comma in a matcher separates
|
|
30
40
|
* names as a `|` does. Its page on debugging a configuration (Check hooks,
|
|
@@ -111,6 +121,19 @@ export declare function mergeClaudeSettings(current: Json, isHook?: (words: read
|
|
|
111
121
|
* say the same twice (ADR-0012).
|
|
112
122
|
*/
|
|
113
123
|
export declare function withSessionStart(settings: Json, isHook: (words: readonly string[]) => boolean): Json | null;
|
|
124
|
+
/**
|
|
125
|
+
* Settings with `onFailure: "block"` on each of init's own entries, under
|
|
126
|
+
* the three events it writes them under, or `null` where each has it
|
|
127
|
+
* already: `init --block-on-failure`. Claude Code's hooks reference
|
|
128
|
+
* (https://code.claude.com/docs/en/hooks, Block the action when a hook
|
|
129
|
+
* fails, read 2026-10-19) has the field read by Claude Code 2.1.295 and
|
|
130
|
+
* later, and a failure under it, a hook that cannot start or that reaches
|
|
131
|
+
* its timeout among them, do what exit 2 does on its event: before a write,
|
|
132
|
+
* hold it. What a release before 2.1.295 does with the key is on no page, so
|
|
133
|
+
* init writes it only where it is asked to (ADR-0012). An entry a person
|
|
134
|
+
* wrote is theirs, and is left as it is.
|
|
135
|
+
*/
|
|
136
|
+
export declare function withOnFailure(settings: Json): Json | null;
|
|
114
137
|
/**
|
|
115
138
|
* Whether settings have Claude Code start the hook as a session starts: an
|
|
116
139
|
* entry under `SessionStart` whose line is init's, or one `isHook` knows
|
|
@@ -118,12 +141,33 @@ export declare function withSessionStart(settings: Json, isHook: (words: readonl
|
|
|
118
141
|
* session starts has the check made for those.
|
|
119
142
|
*/
|
|
120
143
|
export declare function startsSessions(settings: unknown, isHook: (words: readonly string[]) => boolean): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Settings with init's entry under `Stop` added, or `null` where a line
|
|
146
|
+
* of the hook stands there already, init's or one `isHook` knows, which
|
|
147
|
+
* would report the turn twice (ADR-0012).
|
|
148
|
+
*/
|
|
149
|
+
export declare function withTurnReport(settings: Json, isHook: (words: readonly string[]) => boolean): Json | null;
|
|
150
|
+
/** Whether settings have Claude Code start the hook as a turn ends: an entry under `Stop` whose line is init's, or one `isHook` knows as the hook's. */
|
|
151
|
+
export declare function reportsTurns(settings: unknown, isHook: (words: readonly string[]) => boolean): boolean;
|
|
121
152
|
/**
|
|
122
153
|
* What doctor notes where settings hold the guard hooks and none under
|
|
123
154
|
* `SessionStart`: what would hold every write is said at the first write,
|
|
124
155
|
* which it holds, and not as the session starts.
|
|
125
156
|
*/
|
|
126
157
|
export declare const NO_SESSION_START = "note: no settings file has Claude Code start the hook as a session starts, so a configuration that does not load, or a brief named that spec-brief does not list, is said only at the first write, which it holds: spec-harness init --write adds the entry beside the two it wrote";
|
|
158
|
+
/**
|
|
159
|
+
* What doctor notes, run in a linked worktree, of the spec-harness the
|
|
160
|
+
* hook's line runs: the one installed where `${CLAUDE_PROJECT_DIR}` points,
|
|
161
|
+
* which is this work tree in a session started in it, and the main
|
|
162
|
+
* checkout in a worktree session of the desktop app's, where Claude Code
|
|
163
|
+
* reads the hooks from the main checkout's root and points the placeholder
|
|
164
|
+
* at it (its worktrees page, What worktrees share with the main checkout,
|
|
165
|
+
* read 2026-10-19). Said where the two are not one release, `null` for
|
|
166
|
+
* none, and `null` where they are. A note: from 0.19.0 on either decides a
|
|
167
|
+
* write by the work tree that holds it, so it says which release answers,
|
|
168
|
+
* not where a write is decided.
|
|
169
|
+
*/
|
|
170
|
+
export declare function describeInstalls(main: string, there: string | null, here: string | null): string | null;
|
|
127
171
|
/** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */
|
|
128
172
|
export declare function mcpServer(projectDir: string): Json;
|
|
129
173
|
/**
|
|
@@ -198,6 +242,14 @@ export interface HookReport {
|
|
|
198
242
|
/** What `onFailure` changes, where the entry sets it to `"block"`. */
|
|
199
243
|
readonly note: string | null;
|
|
200
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* The release of Claude Code that reads `onFailure` on a hook's entry, by its
|
|
247
|
+
* hooks reference (https://code.claude.com/docs/en/hooks, Command hook
|
|
248
|
+
* fields, read 2026-10-19) and its changelog, which adds the field under it.
|
|
249
|
+
*/
|
|
250
|
+
export declare const ON_FAILURE_SINCE = "2.1.295";
|
|
251
|
+
/** Whether the Claude Code doctor asked its release reads `onFailure`: one that was told, at {@link ON_FAILURE_SINCE} or later. */
|
|
252
|
+
export declare function readsOnFailure(release: ClaudeCodeCheck | null): boolean;
|
|
201
253
|
/** An array that stands for a matcher where it unloads a settings file's hooks: the event it stands under, and the array as written. */
|
|
202
254
|
export interface ArrayMatcher {
|
|
203
255
|
readonly event: string;
|
|
@@ -252,7 +304,7 @@ export declare function handedLines(sources: readonly ClaudeSettings[], project:
|
|
|
252
304
|
* each line of the hook there is started for no write, and one that stood
|
|
253
305
|
* to be started for a write is a guard that is off.
|
|
254
306
|
*/
|
|
255
|
-
export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader, matching?: Matching): HookReport[];
|
|
307
|
+
export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader, matching?: Matching, onFailureHere?: boolean): HookReport[];
|
|
256
308
|
/** Whether a project's `.mcp.json` registers a server under the harness's name, however it runs it. */
|
|
257
309
|
export declare function registersServer(mcp: unknown): boolean;
|
|
258
310
|
/** A Claude Code settings file, named as a person finds it, and what reading it gave: an object, or nothing usable. */
|
|
@@ -411,6 +463,8 @@ export declare function wiringState(wiring: Wired): WiringState;
|
|
|
411
463
|
* checks no write.
|
|
412
464
|
*/
|
|
413
465
|
export declare function guardSwitchedOff(wiring: Wired & Pick<ClaudeCodeWiring, 'hooksOff'>): boolean;
|
|
466
|
+
/** Whether something brings the guard hooks: a settings file that holds them, or a plugin settings turn on that may be on this machine. */
|
|
467
|
+
export declare function guardWired(wiring: Wired): boolean;
|
|
414
468
|
/**
|
|
415
469
|
* What doctor and init say of `disableAllHooks` in `file`. `guarded` is
|
|
416
470
|
* whether there are guard hooks for it to switch off; without any it is a
|
package/dist/configure.js
CHANGED
|
@@ -58,12 +58,28 @@ const GUARDED_EVENTS = [BEFORE, AFTER];
|
|
|
58
58
|
* left out, it is every way a session starts.
|
|
59
59
|
*/
|
|
60
60
|
const AT_START = 'SessionStart';
|
|
61
|
+
/**
|
|
62
|
+
* The event Claude Code runs as its main agent finishes a turn, under which
|
|
63
|
+
* the hook reports what the work tree holds where `turnReport` is on
|
|
64
|
+
* (ADR-0005). Its entry has no matcher: the event takes none.
|
|
65
|
+
*/
|
|
66
|
+
const AT_TURN_END = 'Stop';
|
|
61
67
|
const GUARD_ARGS = Object.freeze([`${PROJECT_DIR}/${BIN}`, 'hook', 'claude']);
|
|
62
68
|
/**
|
|
63
69
|
* The guard as a Claude Code hook: exec form, `args` given, so the project's
|
|
64
70
|
* path is one argument whatever it holds, and no shell is involved.
|
|
65
71
|
*/
|
|
66
72
|
export const GUARD_HOOK = Object.freeze({ type: 'command', command: 'node', args: GUARD_ARGS, timeout: 60 });
|
|
73
|
+
/**
|
|
74
|
+
* The hook's entry at the end of a turn: the guard's line, run in the
|
|
75
|
+
* background. Claude Code waits on no part of it, so the turn ends as it
|
|
76
|
+
* would, and it hands the hook's `systemMessage` to Claude as the next
|
|
77
|
+
* turn begins, where from an entry it waits on it shows the person the
|
|
78
|
+
* message instead (the hooks reference,
|
|
79
|
+
* https://code.claude.com/docs/en/hooks, *Run hooks in the background* and
|
|
80
|
+
* *JSON output*, read 2026-10-19).
|
|
81
|
+
*/
|
|
82
|
+
export const TURN_HOOK = Object.freeze({ ...GUARD_HOOK, async: true });
|
|
67
83
|
// bare-name: what 0.1 installed, an npx command found from wherever the
|
|
68
84
|
// session stood. Read here so that init replaces it, and never run.
|
|
69
85
|
const LEGACY_HOOK = 'npx --no-install spec-harness hook claude';
|
|
@@ -331,6 +347,34 @@ export function withSessionStart(settings, isHook) {
|
|
|
331
347
|
const listed = hooks[AT_START];
|
|
332
348
|
return { ...settings, hooks: { ...hooks, [AT_START]: [...(Array.isArray(listed) ? listed : []), { hooks: [{ ...GUARD_HOOK, args: [...GUARD_ARGS] }] }] } };
|
|
333
349
|
}
|
|
350
|
+
/** The events init writes its own line under. */
|
|
351
|
+
const INIT_EVENTS = [BEFORE, AFTER, AT_START];
|
|
352
|
+
/**
|
|
353
|
+
* Settings with `onFailure: "block"` on each of init's own entries, under
|
|
354
|
+
* the three events it writes them under, or `null` where each has it
|
|
355
|
+
* already: `init --block-on-failure`. Claude Code's hooks reference
|
|
356
|
+
* (https://code.claude.com/docs/en/hooks, Block the action when a hook
|
|
357
|
+
* fails, read 2026-10-19) has the field read by Claude Code 2.1.295 and
|
|
358
|
+
* later, and a failure under it, a hook that cannot start or that reaches
|
|
359
|
+
* its timeout among them, do what exit 2 does on its event: before a write,
|
|
360
|
+
* hold it. What a release before 2.1.295 does with the key is on no page, so
|
|
361
|
+
* init writes it only where it is asked to (ADR-0012). An entry a person
|
|
362
|
+
* wrote is theirs, and is left as it is.
|
|
363
|
+
*/
|
|
364
|
+
export function withOnFailure(settings) {
|
|
365
|
+
const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
|
|
366
|
+
const unblocked = (hook) => isGuard(hook) && hook['onFailure'] !== 'block';
|
|
367
|
+
const next = { ...hooks };
|
|
368
|
+
let changed = false;
|
|
369
|
+
for (const event of INIT_EVENTS) {
|
|
370
|
+
const groups = hooks[event];
|
|
371
|
+
if (!Array.isArray(groups) || !groups.some((group) => hooksOf(group).some(unblocked)))
|
|
372
|
+
continue;
|
|
373
|
+
next[event] = groups.map((group) => (hooksOf(group).some(unblocked) ? { ...group, hooks: hooksOf(group).map((hook) => (unblocked(hook) ? { ...hook, onFailure: 'block' } : hook)) } : group));
|
|
374
|
+
changed = true;
|
|
375
|
+
}
|
|
376
|
+
return changed ? { ...settings, hooks: next } : null;
|
|
377
|
+
}
|
|
334
378
|
/**
|
|
335
379
|
* Whether settings have Claude Code start the hook as a session starts: an
|
|
336
380
|
* entry under `SessionStart` whose line is init's, or one `isHook` knows
|
|
@@ -343,6 +387,25 @@ export function startsSessions(settings, isHook) {
|
|
|
343
387
|
return false;
|
|
344
388
|
return groups.some((group) => hooksOf(group).some((hook) => isObject(hook) && (isGuard(hook) || knownAs(hook, isHook))));
|
|
345
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Settings with init's entry under `Stop` added, or `null` where a line
|
|
392
|
+
* of the hook stands there already, init's or one `isHook` knows, which
|
|
393
|
+
* would report the turn twice (ADR-0012).
|
|
394
|
+
*/
|
|
395
|
+
export function withTurnReport(settings, isHook) {
|
|
396
|
+
if (reportsTurns(settings, isHook))
|
|
397
|
+
return null;
|
|
398
|
+
const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
|
|
399
|
+
const listed = hooks[AT_TURN_END];
|
|
400
|
+
return { ...settings, hooks: { ...hooks, [AT_TURN_END]: [...(Array.isArray(listed) ? listed : []), { hooks: [{ ...TURN_HOOK, args: [...GUARD_ARGS] }] }] } };
|
|
401
|
+
}
|
|
402
|
+
/** Whether settings have Claude Code start the hook as a turn ends: an entry under `Stop` whose line is init's, or one `isHook` knows as the hook's. */
|
|
403
|
+
export function reportsTurns(settings, isHook) {
|
|
404
|
+
const groups = field(field(settings, 'hooks'), AT_TURN_END);
|
|
405
|
+
if (!Array.isArray(groups))
|
|
406
|
+
return false;
|
|
407
|
+
return groups.some((group) => hooksOf(group).some((hook) => isObject(hook) && (isGuard(hook) || knownAs(hook, isHook))));
|
|
408
|
+
}
|
|
346
409
|
/** Whether `isHook` knows an entry's line as the hook's. */
|
|
347
410
|
function knownAs(hook, isHook) {
|
|
348
411
|
const written = writtenHook(hook);
|
|
@@ -354,6 +417,24 @@ function knownAs(hook, isHook) {
|
|
|
354
417
|
* which it holds, and not as the session starts.
|
|
355
418
|
*/
|
|
356
419
|
export const NO_SESSION_START = 'note: no settings file has Claude Code start the hook as a session starts, so a configuration that does not load, or a brief named that spec-brief does not list, is said only at the first write, which it holds: spec-harness init --write adds the entry beside the two it wrote';
|
|
420
|
+
/**
|
|
421
|
+
* What doctor notes, run in a linked worktree, of the spec-harness the
|
|
422
|
+
* hook's line runs: the one installed where `${CLAUDE_PROJECT_DIR}` points,
|
|
423
|
+
* which is this work tree in a session started in it, and the main
|
|
424
|
+
* checkout in a worktree session of the desktop app's, where Claude Code
|
|
425
|
+
* reads the hooks from the main checkout's root and points the placeholder
|
|
426
|
+
* at it (its worktrees page, What worktrees share with the main checkout,
|
|
427
|
+
* read 2026-10-19). Said where the two are not one release, `null` for
|
|
428
|
+
* none, and `null` where they are. A note: from 0.19.0 on either decides a
|
|
429
|
+
* write by the work tree that holds it, so it says which release answers,
|
|
430
|
+
* not where a write is decided.
|
|
431
|
+
*/
|
|
432
|
+
export function describeInstalls(main, there, here) {
|
|
433
|
+
if (there === here)
|
|
434
|
+
return null;
|
|
435
|
+
const release = (version) => (version === null ? 'none' : version);
|
|
436
|
+
return `note: this is a linked worktree of ${main}, and the hook's line runs the spec-harness installed where \${CLAUDE_PROJECT_DIR} points: this work tree's, ${release(here)}, in a session started here, and ${main}'s, ${release(there)}, in one started there, as a worktree session of the desktop app is`;
|
|
437
|
+
}
|
|
357
438
|
/** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */
|
|
358
439
|
export function mcpServer(projectDir) {
|
|
359
440
|
return { command: 'node', args: [`${projectDir}/${BIN}`, 'mcp', '--root', projectDir] };
|
|
@@ -477,17 +558,40 @@ export function isStarted(tools) {
|
|
|
477
558
|
}
|
|
478
559
|
/**
|
|
479
560
|
* The release of Claude Code that reads `onFailure` on a hook's entry, by its
|
|
480
|
-
* hooks reference (https://code.claude.com/docs/en/hooks,
|
|
561
|
+
* hooks reference (https://code.claude.com/docs/en/hooks, Command hook
|
|
562
|
+
* fields, read 2026-10-19) and its changelog, which adds the field under it.
|
|
481
563
|
*/
|
|
482
|
-
const ON_FAILURE_SINCE = '2.1.295';
|
|
483
|
-
/**
|
|
484
|
-
function
|
|
564
|
+
export const ON_FAILURE_SINCE = '2.1.295';
|
|
565
|
+
/** Whether the Claude Code doctor asked its release reads `onFailure`: one that was told, at {@link ON_FAILURE_SINCE} or later. */
|
|
566
|
+
export function readsOnFailure(release) {
|
|
567
|
+
// A release that was told is one whose version was read as one, and the
|
|
568
|
+
// constant is a version, so both parse.
|
|
569
|
+
return release !== null && release.state !== 'unknown' && meets(parseVersion(release.version), parseVersion(ON_FAILURE_SINCE));
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* What `onFailure: "block"` changes for a line, named by `where` it stands,
|
|
573
|
+
* or `null` where the entry does not set it. The hook fails for none of its
|
|
574
|
+
* own answers, a line of its own that turns the guard off among them, which
|
|
575
|
+
* it says with exit 0 (ADR-0005): what the field holds is a hook that does
|
|
576
|
+
* not run.
|
|
577
|
+
*/
|
|
578
|
+
function describeOnFailure(where, blocks) {
|
|
485
579
|
if (!blocks)
|
|
486
580
|
return null;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
581
|
+
return `${where} sets onFailure to "block", so Claude Code ${ON_FAILURE_SINCE} or later holds a write when the hook fails, where it lets the write go ahead without it: by not starting, as where the script on its line is not there, or by running past its timeout`;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* What is said of a line that guards and whose entries set no `onFailure`,
|
|
585
|
+
* where the Claude Code here reads the field: a hook that cannot start lets
|
|
586
|
+
* the write go ahead, and the field would hold it. Advice, which fails
|
|
587
|
+
* nothing: the file is read by every clone's Claude Code, and what a release
|
|
588
|
+
* before {@link ON_FAILURE_SINCE} does with the key is on no page. `own` is
|
|
589
|
+
* whether the entries are init's, in the file init writes, which
|
|
590
|
+
* `init --block-on-failure` gives the field.
|
|
591
|
+
*/
|
|
592
|
+
function adviseOnFailure(where, own) {
|
|
593
|
+
const how = own ? 'spec-harness init --write --block-on-failure writes it on these entries' : 'it is written on each of its entries';
|
|
594
|
+
return `${where} sets no onFailure, so Claude Code lets a write go ahead where the hook cannot start, as where the script on its line is not there, or runs past its timeout: where every Claude Code that reads the file is ${ON_FAILURE_SINCE} or later, as the one here is, "onFailure": "block" holds the write instead, and ${how}`;
|
|
491
595
|
}
|
|
492
596
|
/** Names as a sentence lists them: one, two with "and", more with commas before it. Never asked of none. */
|
|
493
597
|
function named(names) {
|
|
@@ -638,7 +742,7 @@ export function handedLines(sources, project) {
|
|
|
638
742
|
* each line of the hook there is started for no write, and one that stood
|
|
639
743
|
* to be started for a write is a guard that is off.
|
|
640
744
|
*/
|
|
641
|
-
export function reportHooks(sources, project, read, pattern = patternReader(), matching = TODAY) {
|
|
745
|
+
export function reportHooks(sources, project, read, pattern = patternReader(), matching = TODAY, onFailureHere = false) {
|
|
642
746
|
const reading = { pattern, matching };
|
|
643
747
|
return sources.flatMap(({ file, settings }) => {
|
|
644
748
|
const entries = harnessEntries(settings);
|
|
@@ -669,7 +773,10 @@ export function reportHooks(sources, project, read, pattern = patternReader(), m
|
|
|
669
773
|
return [{ ...unloaded, state: 'unstarted', detail: `${said}: the line is not started, and stood to be started for no write as it is, so no guard is switched off` }];
|
|
670
774
|
}
|
|
671
775
|
const written = { file, events: hook.events, line: hook.words, onFailure: hook.blocks ? 'block' : null, before, after, cover: describeCover(where, before, after, mine, reading) };
|
|
672
|
-
const note = describeOnFailure(where, hook.blocks
|
|
776
|
+
const note = describeOnFailure(where, hook.blocks);
|
|
777
|
+
// Where the release here reads the field, a line that guards without it is advised it.
|
|
778
|
+
const own = file === PROJECT_SETTINGS && mine.every((entry) => isGuard(entry.hook));
|
|
779
|
+
const guards = (detail) => ({ ...written, state: 'guards', detail, note: note ?? (onFailureHere ? adviseOnFailure(where, own) : null) });
|
|
673
780
|
if (hook.untyped !== null) {
|
|
674
781
|
const detail = `${where} stands on an entry with ${hook.untyped}, where Claude Code's hooks reference has "type": "command" on an entry it runs as a command, so whether Claude Code runs it cannot be told here: give the entry "type": "command"`;
|
|
675
782
|
return [{ ...written, state: 'unknown', detail, note }];
|
|
@@ -683,15 +790,12 @@ export function reportHooks(sources, project, read, pattern = patternReader(), m
|
|
|
683
790
|
const waited = before === null || before.length > 0;
|
|
684
791
|
if (handed.state === 'off') {
|
|
685
792
|
const detail = `${where}: ${handed.fault.wrong}, so the guard is off and ${handed.unchecked ?? 'no write is checked'}; ${handed.fault.mend}`;
|
|
686
|
-
// A fault the hook says, it says
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
// there, so onFailure holds no write for it, and where the line is
|
|
693
|
-
// waited on before no write there is none it leaves unchecked.
|
|
694
|
-
return [waited ? { ...written, state: 'off', detail, note } : unstarted];
|
|
793
|
+
// A fault the hook says, it says after every write it is started for
|
|
794
|
+
// as well, so it is said wherever the line stands. A root outside
|
|
795
|
+
// any work tree, or in one that does not hold the project, it says
|
|
796
|
+
// nothing of, and where the line is waited on before no write there
|
|
797
|
+
// is none it leaves unchecked.
|
|
798
|
+
return [handed.silent !== true || waited ? { ...written, state: 'off', detail, note } : unstarted];
|
|
695
799
|
}
|
|
696
800
|
if (!waited)
|
|
697
801
|
return [unstarted];
|
|
@@ -703,11 +807,11 @@ export function reportHooks(sources, project, read, pattern = patternReader(), m
|
|
|
703
807
|
return [{ ...written, state: 'unknown', detail: null, note }];
|
|
704
808
|
// What a release wrote: its hook, and nothing after it.
|
|
705
809
|
if (sameStrings(hook.words, ['hook', 'claude']))
|
|
706
|
-
return [
|
|
810
|
+
return [guards(null)];
|
|
707
811
|
const root = handed.root === undefined ? '' : `, and its root, ${handed.root}, is a directory`;
|
|
708
812
|
// Whose briefs a write is checked by, where they are not the project's own.
|
|
709
813
|
const holder = handed.holder === undefined ? '' : ` in ${namedTree(handed.holder)}, which holds this project: the hook checks a write here by that work tree's briefs and its branch, as a file under ${handed.holder.path}/ there`;
|
|
710
|
-
return [
|
|
814
|
+
return [guards(`${where} is more than init writes, and guards: the hook reads the line${root}${holder}`)];
|
|
711
815
|
});
|
|
712
816
|
});
|
|
713
817
|
}
|
|
@@ -936,7 +1040,11 @@ export function wiringState(wiring) {
|
|
|
936
1040
|
* checks no write.
|
|
937
1041
|
*/
|
|
938
1042
|
export function guardSwitchedOff(wiring) {
|
|
939
|
-
return wiring.hooksOff !== null && (wiring
|
|
1043
|
+
return wiring.hooksOff !== null && guardWired(wiring);
|
|
1044
|
+
}
|
|
1045
|
+
/** Whether something brings the guard hooks: a settings file that holds them, or a plugin settings turn on that may be on this machine. */
|
|
1046
|
+
export function guardWired(wiring) {
|
|
1047
|
+
return wiring.hooks.length > 0 || (wiring.plugin !== null && absence(wiring) === null);
|
|
940
1048
|
}
|
|
941
1049
|
/**
|
|
942
1050
|
* What doctor and init say of `disableAllHooks` in `file`. `guarded` is
|