@descent-vtt/spec-harness 0.15.0 → 0.17.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 (44) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +198 -0
  4. package/README.md +18 -3
  5. package/dist/branch.js +12 -0
  6. package/dist/branch.js.map +1 -1
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.js +76 -56
  9. package/dist/cli.js.map +1 -1
  10. package/dist/configure.d.ts +200 -20
  11. package/dist/configure.js +514 -69
  12. package/dist/configure.js.map +1 -1
  13. package/dist/hooks.d.ts +17 -1
  14. package/dist/hooks.js +17 -1
  15. package/dist/hooks.js.map +1 -1
  16. package/dist/reader.js +5 -1
  17. package/dist/reader.js.map +1 -1
  18. package/dist/round.d.ts +35 -6
  19. package/dist/round.js +137 -17
  20. package/dist/round.js.map +1 -1
  21. package/dist/rulings.js +6 -0
  22. package/dist/rulings.js.map +1 -1
  23. package/dist/setup.d.ts +1 -1
  24. package/dist/setup.js +72 -17
  25. package/dist/setup.js.map +1 -1
  26. package/dist/signers.js +5 -1
  27. package/dist/signers.js.map +1 -1
  28. package/dist/usage.d.ts +48 -8
  29. package/dist/usage.js +35 -1
  30. package/dist/usage.js.map +1 -1
  31. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -1
  32. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  33. package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
  34. package/dist/vendor/spec-core/markdown/links.js.map +1 -1
  35. package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
  36. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
  37. package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
  38. package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
  39. package/dist/vendor/spec-core/text/lines.js.map +1 -1
  40. package/dist/vendor/spec-core/text/width.js.map +1 -1
  41. package/dist/workspace.d.ts +12 -3
  42. package/dist/workspace.js +20 -3
  43. package/dist/workspace.js.map +1 -1
  44. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rulings.js","sourceRoot":"","sources":["../src/rulings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Escalations an agent raises and the rulings a person signs.\n *\n * A brief says what a round may not touch. When the round cannot be done\n * without touching it, the agent stops and asks; a person decides; and the\n * decision becomes a row in the brief's rulings table:\n *\n * ```markdown\n * ## Rulings\n *\n * | Ruling | Paths | Decision | Note |\n * | --- | --- | --- | --- |\n * | R-012-1 | `src/db/schema.ts` | allow | Add rotated_at; no other schema change. |\n * ```\n *\n * A row is text, and an agent can write text. What makes a row a ruling is\n * the commit that last changed it: signed, with a key listed in the allowed\n * signers file as the base branch has it (ADR-0006). The row states the\n * decision; the signature says a person made it; the base branch says whose\n * signatures count. A hash of the row, which the agent could compute as well\n * as anyone, would say none of that.\n *\n * The request itself lives outside the work tree while it waits\n * (`<git-common-dir>/spec-harness/escalations/`): it is in-flight state, and\n * only its outcome belongs in the record (spec-core ADR-0005).\n */\n\nexport interface EscalationOption {\n readonly label: string;\n readonly consequence: string;\n}\n\nexport interface EscalationRequest {\n readonly id: string;\n readonly brief: string;\n readonly briefFile: string;\n readonly paths: readonly string[];\n readonly reason: string;\n readonly options: readonly EscalationOption[];\n readonly recommendation: string | null;\n readonly created: string;\n readonly branch: string | null;\n readonly head: string | null;\n}\n\nexport interface RulingRow {\n readonly id: string;\n readonly paths: readonly string[];\n readonly decision: 'allow' | 'deny';\n readonly note: string;\n /** 1-based line of the row in the brief. */\n readonly line: number;\n}\n\n/** A table as the Markdown scanner reads it: header cells, and rows with their lines. */\nexport interface TableView {\n readonly headers: readonly string[];\n readonly rows: readonly { readonly cells: readonly string[]; readonly line: number }[];\n}\n\nexport type RulingProblem = { readonly line: number; readonly message: string };\n\nfunction normal(text: string): string {\n return text.replace(/[*_`]/g, '').trim().toLowerCase();\n}\n\n/** The globs a paths cell names: backtick-quoted, or comma-separated. */\nexport function pathsOf(cell: string): string[] {\n const quoted = [...cell.matchAll(/`([^`]+)`/g)].map((match) => (match[1] as string).trim());\n if (quoted.length > 0) return quoted.filter((path) => path !== '');\n return cell\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '');\n}\n\n/**\n * The rulings in a brief's rulings section, from its tables. A table whose\n * header does not name the columns is not a rulings table; a row that cannot\n * be read is a problem to report, never a ruling.\n */\nexport function readRulings(tables: readonly TableView[]): { rulings: RulingRow[]; problems: RulingProblem[] } {\n const rulings: RulingRow[] = [];\n const problems: RulingProblem[] = [];\n for (const table of tables) {\n const headers = table.headers.map(normal);\n const at = (...names: string[]): number => headers.findIndex((header) => names.includes(header));\n const idColumn = at('ruling', 'id');\n const pathsColumn = at('paths', 'path', 'files');\n const decisionColumn = at('decision');\n const noteColumn = at('note', 'notes', 'reason');\n if (idColumn < 0 || pathsColumn < 0 || decisionColumn < 0) continue;\n for (const row of table.rows) {\n const id = normal(row.cells[idColumn] ?? '').toUpperCase();\n const paths = pathsOf(row.cells[pathsColumn] ?? '');\n const decision = normal(row.cells[decisionColumn] ?? '');\n if (id === '') {\n problems.push({ line: row.line, message: 'a ruling row has no id' });\n continue;\n }\n if (paths.length === 0) {\n problems.push({ line: row.line, message: `ruling ${id} names no path` });\n continue;\n }\n if (decision !== 'allow' && decision !== 'deny') {\n problems.push({ line: row.line, message: `ruling ${id} decides \"${decision}\"; a decision is allow or deny` });\n continue;\n }\n if (rulings.some((ruling) => ruling.id === id)) {\n problems.push({ line: row.line, message: `ruling ${id} appears twice` });\n continue;\n }\n rulings.push({ id, paths, decision, note: (row.cells[noteColumn] ?? '').trim(), line: row.line });\n }\n }\n return { rulings, problems };\n}\n\n/**\n * The next free escalation id for a brief, given the ids already taken. The\n * stem compares without case: `readRulings` reads ids in capitals, so a brief\n * named `auth` has `R-AUTH-1` taken when this writes `R-auth-2`.\n */\nexport function nextId(prefix: 'E' | 'R', brief: string, taken: readonly string[]): string {\n let highest = 0;\n const stem = `${prefix}-${brief}-`;\n for (const id of taken) {\n if (!id.toUpperCase().startsWith(stem.toUpperCase())) continue;\n const n = Number(id.slice(stem.length));\n if (Number.isInteger(n) && n > highest) highest = n;\n }\n return `${stem}${highest + 1}`;\n}\n\n/** The memo a person reads to rule: what, why, the choices, and what each costs. */\nexport function renderMemo(request: EscalationRequest): string {\n const lines = [\n `# Escalation ${request.id}`,\n '',\n `Brief ${request.brief} (\\`${request.briefFile}\\`)${request.branch === null ? '' : ` on \\`${request.branch}\\``}${request.head === null ? '' : ` at \\`${request.head.slice(0, 12)}\\``}, raised ${request.created}.`,\n '',\n '## What the round needs to change',\n '',\n ...request.paths.map((path) => `- \\`${path}\\``),\n '',\n '## Why',\n '',\n request.reason.trim(),\n '',\n ];\n if (request.options.length > 0) {\n lines.push('## Options', '');\n // A choice whose cost was not given is written as the choice alone,\n // with no dash that leads nowhere.\n request.options.forEach((option, index) => lines.push(`${index + 1}. **${option.label}**${option.consequence === '' ? '' : ` - ${option.consequence}`}`));\n lines.push('');\n }\n if (request.recommendation !== null) lines.push('## The agent recommends', '', request.recommendation.trim(), '');\n lines.push(\n '## To rule',\n '',\n `Allow: \\`spec-harness rule ${request.id} --allow --note \"<what exactly is allowed>\"\\``,\n `Refuse: \\`spec-harness rule ${request.id} --deny --note \"<why>\"\\``,\n '',\n 'Either writes a row into the brief\\'s rulings table. Commit it signed (`git commit -S`) with a key the base branch lists in its allowed signers; an unsigned row allows nothing.',\n '',\n );\n return lines.join('\\n');\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The table row a ruling writes. */\nexport function renderRow(id: string, paths: readonly string[], decision: 'allow' | 'deny', note: string): string {\n return `| ${id} | ${paths.map((path) => `\\`${cell(path)}\\``).join(', ')} | ${decision} | ${cell(note)} |`;\n}\n\n/**\n * The brief with a ruling row added: under the rulings section's table when\n * there is one, or in a new section at the end. Every other line is left as\n * it was; line endings are the brief's own.\n */\nexport function addRulingRow(\n text: string,\n section: string,\n row: string,\n where: { readonly headingLine: number | null; readonly tableEndLine: number | null },\n): string {\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const lines = text.split(/\\r?\\n/);\n const trailing = lines.length > 0 && lines[lines.length - 1] === '' ? lines.pop() : undefined;\n const header = ['| Ruling | Paths | Decision | Note |', '| --- | --- | --- | --- |'];\n if (where.tableEndLine !== null) {\n lines.splice(where.tableEndLine, 0, row);\n } else if (where.headingLine !== null) {\n // A line of prose right under the table would read as one more row.\n const next = lines[where.headingLine];\n const gap = next !== undefined && next.trim() !== '' ? [''] : [];\n lines.splice(where.headingLine, 0, '', ...header, row, ...gap);\n } else {\n if (lines.length > 0 && lines[lines.length - 1]?.trim() !== '') lines.push('');\n lines.push(`## ${section}`, '', ...header, row);\n }\n if (trailing !== undefined) lines.push(trailing);\n return lines.join(eol);\n}\n"]}
1
+ {"version":3,"file":"rulings.js","sourceRoot":"","sources":["../src/rulings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Escalations an agent raises and the rulings a person signs.\n *\n * A brief says what a round may not touch. When the round cannot be done\n * without touching it, the agent stops and asks; a person decides; and the\n * decision becomes a row in the brief's rulings table:\n *\n * ```markdown\n * ## Rulings\n *\n * | Ruling | Paths | Decision | Note |\n * | --- | --- | --- | --- |\n * | R-012-1 | `src/db/schema.ts` | allow | Add rotated_at; no other schema change. |\n * ```\n *\n * A row is text, and an agent can write text. What makes a row a ruling is\n * the commit that last changed it: signed, with a key listed in the allowed\n * signers file as the base branch has it (ADR-0006). The row states the\n * decision; the signature says a person made it; the base branch says whose\n * signatures count. A hash of the row, which the agent could compute as well\n * as anyone, would say none of that.\n *\n * The request itself lives outside the work tree while it waits\n * (`<git-common-dir>/spec-harness/escalations/`): it is in-flight state, and\n * only its outcome belongs in the record (spec-core ADR-0005).\n */\n\nexport interface EscalationOption {\n readonly label: string;\n readonly consequence: string;\n}\n\nexport interface EscalationRequest {\n readonly id: string;\n readonly brief: string;\n readonly briefFile: string;\n readonly paths: readonly string[];\n readonly reason: string;\n readonly options: readonly EscalationOption[];\n readonly recommendation: string | null;\n readonly created: string;\n readonly branch: string | null;\n readonly head: string | null;\n}\n\nexport interface RulingRow {\n readonly id: string;\n readonly paths: readonly string[];\n readonly decision: 'allow' | 'deny';\n readonly note: string;\n /** 1-based line of the row in the brief. */\n readonly line: number;\n}\n\n/** A table as the Markdown scanner reads it: header cells, and rows with their lines. */\nexport interface TableView {\n readonly headers: readonly string[];\n readonly rows: readonly { readonly cells: readonly string[]; readonly line: number }[];\n}\n\nexport type RulingProblem = { readonly line: number; readonly message: string };\n\nfunction normal(text: string): string {\n return text.replace(/[*_`]/g, '').trim().toLowerCase();\n}\n\n/** The globs a paths cell names: backtick-quoted, or comma-separated. */\nexport function pathsOf(cell: string): string[] {\n const quoted = [...cell.matchAll(/`([^`]+)`/g)].map((match) => (match[1] as string).trim());\n if (quoted.length > 0) return quoted.filter((path) => path !== '');\n return cell\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '');\n}\n\n/**\n * The rulings in a brief's rulings section, from its tables. A table whose\n * header does not name the columns is not a rulings table; a row that cannot\n * be read is a problem to report, never a ruling.\n */\nexport function readRulings(tables: readonly TableView[]): { rulings: RulingRow[]; problems: RulingProblem[] } {\n const rulings: RulingRow[] = [];\n const problems: RulingProblem[] = [];\n for (const table of tables) {\n const headers = table.headers.map(normal);\n const at = (...names: string[]): number => headers.findIndex((header) => names.includes(header));\n const idColumn = at('ruling', 'id');\n const pathsColumn = at('paths', 'path', 'files');\n const decisionColumn = at('decision');\n const noteColumn = at('note', 'notes', 'reason');\n if (idColumn < 0 || pathsColumn < 0 || decisionColumn < 0) continue;\n for (const row of table.rows) {\n const id = normal(row.cells[idColumn] ?? '').toUpperCase();\n const paths = pathsOf(row.cells[pathsColumn] ?? '');\n const decision = normal(row.cells[decisionColumn] ?? '');\n if (id === '') {\n problems.push({ line: row.line, message: 'a ruling row has no id' });\n continue;\n }\n if (paths.length === 0) {\n problems.push({ line: row.line, message: `ruling ${id} names no path` });\n continue;\n }\n if (decision !== 'allow' && decision !== 'deny') {\n problems.push({ line: row.line, message: `ruling ${id} decides \"${decision}\"; a decision is allow or deny` });\n continue;\n }\n if (rulings.some((ruling) => ruling.id === id)) {\n problems.push({ line: row.line, message: `ruling ${id} appears twice` });\n continue;\n }\n rulings.push({ id, paths, decision, note: (row.cells[noteColumn] ?? '').trim(), line: row.line });\n }\n }\n return { rulings, problems };\n}\n\n/**\n * The next free escalation id for a brief, given the ids already taken. The\n * stem compares without case: `readRulings` reads ids in capitals, so a brief\n * named `auth` has `R-AUTH-1` taken when this writes `R-auth-2`.\n */\nexport function nextId(prefix: 'E' | 'R', brief: string, taken: readonly string[]): string {\n let highest = 0;\n const stem = `${prefix}-${brief}-`;\n for (const id of taken) {\n if (!id.toUpperCase().startsWith(stem.toUpperCase())) continue;\n const n = Number(id.slice(stem.length));\n // A number equal to the highest is the highest already, so `>=` for `>`\n // is equivalent.\n if (Number.isInteger(n) && n > highest) highest = n;\n }\n return `${stem}${highest + 1}`;\n}\n\n/** The memo a person reads to rule: what, why, the choices, and what each costs. */\nexport function renderMemo(request: EscalationRequest): string {\n const lines = [\n `# Escalation ${request.id}`,\n '',\n `Brief ${request.brief} (\\`${request.briefFile}\\`)${request.branch === null ? '' : ` on \\`${request.branch}\\``}${request.head === null ? '' : ` at \\`${request.head.slice(0, 12)}\\``}, raised ${request.created}.`,\n '',\n '## What the round needs to change',\n '',\n ...request.paths.map((path) => `- \\`${path}\\``),\n '',\n '## Why',\n '',\n request.reason.trim(),\n '',\n ];\n if (request.options.length > 0) {\n lines.push('## Options', '');\n // A choice whose cost was not given is written as the choice alone,\n // with no dash that leads nowhere.\n request.options.forEach((option, index) => lines.push(`${index + 1}. **${option.label}**${option.consequence === '' ? '' : ` - ${option.consequence}`}`));\n lines.push('');\n }\n if (request.recommendation !== null) lines.push('## The agent recommends', '', request.recommendation.trim(), '');\n lines.push(\n '## To rule',\n '',\n `Allow: \\`spec-harness rule ${request.id} --allow --note \"<what exactly is allowed>\"\\``,\n `Refuse: \\`spec-harness rule ${request.id} --deny --note \"<why>\"\\``,\n '',\n 'Either writes a row into the brief\\'s rulings table. Commit it signed (`git commit -S`) with a key the base branch lists in its allowed signers; an unsigned row allows nothing.',\n '',\n );\n return lines.join('\\n');\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The table row a ruling writes. */\nexport function renderRow(id: string, paths: readonly string[], decision: 'allow' | 'deny', note: string): string {\n return `| ${id} | ${paths.map((path) => `\\`${cell(path)}\\``).join(', ')} | ${decision} | ${cell(note)} |`;\n}\n\n/**\n * The brief with a ruling row added: under the rulings section's table when\n * there is one, or in a new section at the end. Every other line is left as\n * it was; line endings are the brief's own.\n */\nexport function addRulingRow(\n text: string,\n section: string,\n row: string,\n where: { readonly headingLine: number | null; readonly tableEndLine: number | null },\n): string {\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const lines = text.split(/\\r?\\n/);\n // A text splits into one line at least, so the first check here is the\n // compiler's, and its mutants are equivalent.\n const trailing = lines.length > 0 && lines[lines.length - 1] === '' ? lines.pop() : undefined;\n const header = ['| Ruling | Paths | Decision | Note |', '| --- | --- | --- | --- |'];\n if (where.tableEndLine !== null) {\n lines.splice(where.tableEndLine, 0, row);\n } else if (where.headingLine !== null) {\n // A line of prose right under the table would read as one more row.\n const next = lines[where.headingLine];\n const gap = next !== undefined && next.trim() !== '' ? [''] : [];\n lines.splice(where.headingLine, 0, '', ...header, row, ...gap);\n } else {\n // The last line is read only where there is one, so the `?.` is the\n // compiler's, and the mutant without it is equivalent.\n if (lines.length > 0 && lines[lines.length - 1]?.trim() !== '') lines.push('');\n lines.push(`## ${section}`, '', ...header, row);\n }\n if (trailing !== undefined) lines.push(trailing);\n return lines.join(eol);\n}\n"]}
package/dist/setup.d.ts CHANGED
@@ -32,5 +32,5 @@ export declare function gitHook(root: string): Promise<{
32
32
  readonly path: string | null;
33
33
  readonly note: string | null;
34
34
  }>;
35
- export declare function plan(workspace: Workspace, options: Options, env: CliIO['env']): Promise<Step[]>;
35
+ export declare function plan(workspace: Workspace, options: Options, io: CliIO): Promise<Step[]>;
36
36
  export declare function initCommand(options: Options, io: CliIO): Promise<number>;
package/dist/setup.js CHANGED
@@ -15,12 +15,14 @@ import { existsSync, statSync } from 'node:fs';
15
15
  import { chmod, mkdir, readFile } from 'node:fs/promises';
16
16
  import { dirname, isAbsolute, join, relative, sep } from 'node:path';
17
17
  import { CONFIG_FILE } from './config.js';
18
- import { chooseBase, describeGitHook, describeGraph, describeSkipped, enabledPlugin, GIT_HOOK_LINE, gitHookNote, graphReadsBriefs, GUARD_HOOK, holdsGuard, loadsPlugin, measuresArchive, mergeClaudeSettings, mergeMcp, mergeSpecBrief, mergeSpecGraph, missingGitHook, PLUGIN, PRE_COMMIT, PROTECT_RULE_FILES, registersServer, SPEC_BRIEF_CONFIGS, SPEC_GRAPH_CONFIGS, } from './configure.js';
18
+ import { chooseBase, describeAbsent, describeGitHook, describeGraph, describeHooksOff, describePresence, describeSkipped, GIT_HOOK_LINE, gitHookNote, graphReadsBriefs, GUARD_HOOK, guardSwitchedOff, isStarted, loadsPlugin, measuresArchive, mergeClaudeSettings, mergeMcp, mergeSpecBrief, mergeSpecGraph, missingGitHook, patternReader, PLUGIN, PRE_COMMIT, PROJECT_SETTINGS, PROTECT_RULE_FILES, registersServer, SPEC_BRIEF_CONFIGS, SPEC_GRAPH_CONFIGS, } from './configure.js';
19
19
  import { readJsonObject as readJson, writeAtomic } from './fs.js';
20
20
  import { hookPath, localBranches, remoteDefault } from './git.js';
21
- import { claudeSettings, pluginFile } from './round.js';
21
+ import { claudeCodeWiring, pluginFile } from './round.js';
22
22
  import { runSibling } from './siblings.js';
23
- import { EXIT_ERROR, EXIT_OK, json, openWorkspace } from './workspace.js';
23
+ import { answerOf, EXIT_ERROR, EXIT_OK, json, openWorkspace } from './workspace.js';
24
+ /** The project's settings file for Claude Code, as a person finds it and as doctor names it. */
25
+ const SETTINGS = PROJECT_SETTINGS;
24
26
  function stringify(value) {
25
27
  return `${JSON.stringify(value, null, 2)}\n`;
26
28
  }
@@ -59,7 +61,7 @@ export async function gitHook(root) {
59
61
  const inert = process.platform !== 'win32' && (statSync(path).mode & 0o111) === 0;
60
62
  return { hook: { state: inert ? 'inert' : 'runs', file }, path, note: gitHookNote(text) };
61
63
  }
62
- export async function plan(workspace, options, env) {
64
+ export async function plan(workspace, options, io) {
63
65
  const { root } = workspace;
64
66
  const steps = [];
65
67
  // The base rounds merge into, decided once: spec-harness verifies rulings
@@ -70,7 +72,20 @@ export async function plan(workspace, options, env) {
70
72
  const harnessFile = join(root, CONFIG_FILE);
71
73
  const harness = (await readJson(harnessFile));
72
74
  const namedBase = harness !== null && typeof harness['base'] === 'string' ? harness['base'] : null;
73
- const choice = namedBase === null ? chooseBase({ remoteDefault: await remoteDefault(root), branch: workspace.branch, branches: await localBranches(root) }) : null;
75
+ // What the plan asks of git and of Claude Code's settings waits on the
76
+ // root and on nothing of the rest, so it is asked at once: three of git's
77
+ // processes side by side where each waited on the one before. Each is
78
+ // read below where it was asked before, so a plan that fails still fails
79
+ // on the first of them in that order. The matchers in the settings are
80
+ // read with one reader, for the wiring and for what to write.
81
+ const pattern = patternReader();
82
+ const [factsAsked, wiringAsked, hookAsked] = await Promise.allSettled([
83
+ namedBase === null ? Promise.all([remoteDefault(root), localBranches(root)]) : null,
84
+ claudeCodeWiring(root, io, pattern),
85
+ gitHook(root),
86
+ ]);
87
+ const facts = answerOf(factsAsked);
88
+ const choice = facts === null ? null : chooseBase({ remoteDefault: facts[0], branch: workspace.branch, branches: facts[1] });
74
89
  const base = choice === null ? namedBase : choice.base;
75
90
  // spec-brief first: every other setting is derived from its directories.
76
91
  const briefConfig = SPEC_BRIEF_CONFIGS.find((name) => existsSync(join(root, name)));
@@ -185,34 +200,74 @@ export async function plan(workspace, options, env) {
185
200
  }
186
201
  // The agent's hooks and server, unless the plugin brings them: Claude Code
187
202
  // runs a plugin's hook beside the same hook in settings, so both would
188
- // guard every write twice (ADR-0012).
189
- const plugin = enabledPlugin(await claudeSettings(root, env));
203
+ // guard every write twice (ADR-0012). The hook's lines are known as doctor
204
+ // knows them, by the hook's own reading and by the writes Claude Code
205
+ // starts each for, so that the two say the same of every file (ADR-0005).
206
+ const wiring = answerOf(wiringAsked);
207
+ const { plugin } = wiring;
208
+ // A plugin that is not on this machine brings neither. init writes none
209
+ // on that reading alone, which a plugin that is there after all would
210
+ // make a second guard: it says what doctor says, and how to have the
211
+ // entries here instead.
212
+ const absent = wiring.presence?.state === 'absent' ? wiring.presence.detail : null;
190
213
  const settingsFile = join(root, '.claude', 'settings.json');
191
214
  const settings = await readJson(settingsFile);
215
+ const lines = wiring.lines.filter((line) => line.file === SETTINGS);
216
+ // What doctor says of a line this file holds is said here too, with why
217
+ // init leaves the line: one a person changed is theirs, and a second line
218
+ // beside one that is started would run for every write as well.
219
+ const said = lines.flatMap((line) => [line.detail, line.cover]).filter((note) => note !== null);
220
+ const left = said.length === 0 ? '' : `${said.join('; ')}; init rewrites no line of the hook it did not write, and writes none beside one Claude Code starts for a write`;
192
221
  if (plugin !== null) {
193
- const present = holdsGuard(settings);
194
- steps.push({ file: '.claude/settings.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'hooks', present) });
222
+ // Doubled in this file or in one of the person's own, it is the double
223
+ // install doctor fails: said here too, though init writes neither file.
224
+ // This file's own entry is said first, so the others are named only
225
+ // where it holds none.
226
+ const present = wiring.hooks.includes(SETTINGS);
227
+ if (absent !== null)
228
+ steps.push({ file: SETTINGS, action: 'advise', detail: describeAbsent(plugin, absent, 'hooks', present) });
229
+ else {
230
+ // A plugin that cannot be told to be on this machine is no reason to
231
+ // write either: said beside the reason, as doctor says it.
232
+ const untold = describePresence(wiring);
233
+ const detail = describeSkipped(plugin, 'hooks', present, wiring.hooks);
234
+ steps.push({ file: SETTINGS, action: wiring.hooks.length > 0 ? 'advise' : 'skip', detail: untold === null ? detail : `${detail}. ${untold}` });
235
+ }
195
236
  }
196
237
  else if (settings === 'unreadable') {
197
- steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });
238
+ steps.push({ file: SETTINGS, action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });
198
239
  }
199
240
  else {
200
- const merged = mergeClaudeSettings(settings ?? {});
241
+ // An entry is a line of the hook where doctor reports its words as one in this file.
242
+ const reported = new Set(lines.map((line) => JSON.stringify(line.line)));
243
+ const merged = mergeClaudeSettings(settings ?? {}, (words) => reported.has(JSON.stringify(words)), pattern);
244
+ // A line that turns the guard off or holds every write is one doctor
245
+ // fails, and lines of which none is waited on before a write are no
246
+ // guard between them: left as they are, they are for the person to mend.
247
+ const broken = lines.some((line) => line.state === 'off' || line.state === 'held') || !lines.some((line) => isStarted(line.before));
201
248
  if (merged === null)
202
- steps.push({ file: '.claude/settings.json', action: 'keep', detail: 'the guard hooks are installed' });
249
+ steps.push({ file: SETTINGS, action: broken ? 'advise' : 'keep', detail: left === '' ? 'the guard hooks are installed' : left });
203
250
  else
204
251
  steps.push({
205
- file: '.claude/settings.json',
252
+ file: SETTINGS,
206
253
  action: settings === null ? 'create' : 'update',
207
- detail: 'the guard hooks, run with node from the project\'s install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope',
254
+ detail: `the guard hooks, run with node from the project's install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope${left === '' ? '' : `; ${left}`}`,
208
255
  apply: () => writeAtomic(settingsFile, stringify(merged)),
209
256
  });
210
257
  }
258
+ // disableAllHooks turns off whatever hooks there are, and those init
259
+ // writes with them: said of the file that holds the key, which is the
260
+ // person's to change. init writes what it would have written.
261
+ if (wiring.hooksOff !== null)
262
+ steps.push({ file: wiring.hooksOff, action: 'advise', detail: describeHooksOff(wiring.hooksOff, guardSwitchedOff(wiring)) });
211
263
  const mcpFile = join(root, '.mcp.json');
212
264
  const mcp = await readJson(mcpFile);
213
265
  if (plugin !== null) {
214
266
  const present = registersServer(mcp);
215
- steps.push({ file: '.mcp.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'server', present) });
267
+ if (absent !== null)
268
+ steps.push({ file: '.mcp.json', action: 'advise', detail: describeAbsent(plugin, absent, 'server', present) });
269
+ else
270
+ steps.push({ file: '.mcp.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'server', present) });
216
271
  }
217
272
  else if (mcp === 'unreadable') {
218
273
  steps.push({ file: '.mcp.json', action: 'advise', detail: 'cannot be read as JSON; add the spec-harness server by hand' });
@@ -233,7 +288,7 @@ export async function plan(workspace, options, env) {
233
288
  // a linked worktree's shared hooks. It is the guard for a write the agent's
234
289
  // hooks never see, one through a shell, so it is advised when not asked
235
290
  // for; it is configuration outside the tree, so it is written only then.
236
- const { hook, path: hookFile, note: hookNote } = await gitHook(root);
291
+ const { hook, path: hookFile, note: hookNote } = answerOf(hookAsked);
237
292
  // Only a git older than 2.31, which the README states as the harness's
238
293
  // floor, cannot name its hooks, and init runs only in a work tree, where
239
294
  // any later git names them: no test reaches this advice, by that decision.
@@ -277,7 +332,7 @@ export async function plan(workspace, options, env) {
277
332
  }
278
333
  export async function initCommand(options, io) {
279
334
  const workspace = await openWorkspace(options, io);
280
- const steps = await plan(workspace, options, io.env);
335
+ const steps = await plan(workspace, options, io);
281
336
  if (options.write) {
282
337
  for (const step of steps) {
283
338
  if (step.apply !== undefined) {
package/dist/setup.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,eAAe,EACf,aAAa,EACb,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EAEX,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,UAAU,EACV,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,cAAc,IAAI,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAA4C,MAAM,gBAAgB,CAAC;AAcpH,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED,MAAM,aAAa,GAAG,gCAAgC,MAAM,kHAAkH,CAAC;AAE/K,4EAA4E;AAC5E,SAAS,eAAe,CAAC,MAAe,EAAE,IAAmB;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,IAAI,IAAI,KAAK,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,4BAA4B,IAAI,gIAAgI,CAAC,CAAC;IAChM,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,gHAAgH;AAChH,SAAS,KAAK,CAAC,IAAY,EAAE,IAAY;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,uEAAuE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5J,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACpF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAChG,0EAA0E;IAC1E,sCAAsC;IACtC,8FAA8F;IAC9F,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAClF,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;AAC5F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,SAAoB,EAAE,OAAgB,EAAE,GAAiB;IAClF,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC;IAC3B,MAAM,KAAK,GAAW,EAAE,CAAC;IAEzB,0EAA0E;IAC1E,2EAA2E;IAC3E,iEAAiE;IACjE,4EAA4E;IAC5E,uCAAuC;IACvC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAgB,CAAC;IAC7D,MAAM,SAAS,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnG,MAAM,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnK,MAAM,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;IAEvD,yEAAyE;IACzE,MAAM,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtD,IAAI,MAAM,GAAG,QAAQ,CAAC;IACtB,IAAI,OAAO,GAAG,gBAAgB,CAAC;IAC/B,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,+EAA+E;gBACvF,KAAK,EAAE,KAAK,IAAI,EAAE;oBAChB,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;oBACtD,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;wBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACtF,CAAC;aACF,CAAC,CAAC;YACH,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC;gBACnC,KAAK,EAAE,KAAK,IAAI,EAAE;oBAChB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;oBACxC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,YAAY;wBAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;oBAC1H,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;oBAC/D,IAAI,MAAM,KAAK,IAAI;wBAAE,MAAM,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;gBACrE,CAAC;aACF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,0EAA0E;QAC1E,oEAAoE;QACpE,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;YAC/C,IAAI,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ;gBAAE,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YACpE,OAAO,GAAG,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,UAAU,CAAC;QAC5F,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,MAAM,qBAAqB,OAAO,GAAG,EAAE,CAAC,CAAC;QAC9G,2EAA2E;QAC3E,8CAA8C;QAC9C,IAAI,MAAM,KAAK,YAAY,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iCAAiC,IAAI,KAAK,CAAC;YAChF,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,4CAA4C,MAAM,KAAK,OAAO,UAAU,EAAE,CAAC,CAAC;QACxI,CAAC;aAAM,CAAC;YACN,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;YAC1D,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;YACzC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;gBACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,sCAAsC,MAAM,CAAE,SAAkB,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzG,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kCAAkC,IAAI,EAAE,EAAE,CAAC,CAAC;YACtG,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC3F,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;YACjH,CAAC;QACH,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,yEAAyE;IACzE,gDAAgD;IAChD,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAChH,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,MAAM,WAAW,GAAG,GAAG,OAAO,KAAK,CAAC;QACpC,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAClF,MAAM,QAAQ,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7F,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,YAAY,IAAI,YAAY,IAAI,QAAQ,EAAE,CAAC;YAC/E,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,qGAAqG,WAAW,oCAAoC,EAAE,CAAC,CAAC;QACvN,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,SAAS,IAAI,kBAAkB,CAAC;YAC7C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACjD,IAAI,OAAO,KAAK,YAAY,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,oDAAoD,WAAW,YAAY,EAAE,CAAC,CAAC;YAC9H,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC;gBAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,gBAAgB,CAAC,OAAO,IAAI,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;gBAC5G,IAAI,MAAM,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;;oBAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;YACnJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,2EAA2E;IAC3E,oCAAoC;IACpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,SAAS,EAAE,EAAE,CAAC,CAAC;IACrG,CAAC;SAAM,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,GAAS,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,WAAW,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACtK,CAAC;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,sCAAsC;IACtC,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxI,CAAC;SAAM,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,yFAAyF,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;IACjM,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;QACnD,IAAI,MAAM,KAAK,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,+BAA+B,EAAE,CAAC,CAAC;;YAE1H,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,uBAAuB;gBAC7B,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;gBAC/C,MAAM,EAAE,0KAA0K;gBAClL,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;aAC1D,CAAC,CAAC;IACP,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7H,CAAC;SAAM,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,6DAA6D,EAAE,CAAC,CAAC;IAC7H,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC,CAAC;;YAEtH,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;gBAC1C,MAAM,EAAE,+IAA+I;gBACvJ,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;aACrD,CAAC,CAAC;IACP,CAAC;IAED,0EAA0E;IAC1E,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,uEAAuE;IACvE,yEAAyE;IACzE,2EAA2E;IAC3E,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;SAAM,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QACjC,uEAAuE;QACvE,0DAA0D;QAC1D,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,8BAA8B,QAAQ,EAAE,EAAE,CAAC,CAAC;IACtJ,CAAC;SAAM,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC;SAAM,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,2CAA2C,aAAa,SAAS,EAAE,CAAC,CAAC;IAC/H,CAAC;SAAM,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,6DAA6D;YACrE,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpD,MAAM,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;gBACxC,MAAM,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC/B,CAAC;SACF,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,0EAA0E;IAC1E,yCAAyC;IACzC,KAAK,CAAC,IAAI,CAAC;QACT,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc;QAC7C,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,8QAA8Q,kBAAkB,EAAE;KAC3S,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAgB,EAAE,EAAS;IAC3D,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;gBACrB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC,IAAI,KAAM,KAAe,CAAC,OAAO,IAAI,CAAC,CAAC;oBAC7E,OAAO,UAAU,CAAC;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxI,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,aAAa,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;QAC9G,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;YAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,oEAAoE,CAAC,CAAC;IAC9J,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * `spec-harness init`: the family configured to agree, from the first day.\n *\n * The integration faults found when the tools were first run together were all\n * configuration: spec-graph read an archived brief as a retired decision and\n * reported every brief that depended on one; spec-guard executed an archived\n * brief's assertions. Each tool can be told the truth; `init` tells each of\n * them the same truth, derived from spec-brief's own settings.\n *\n * It prints a plan and changes nothing until `--write`. It merges into files\n * that exist and never replaces what a person wrote; a git hook the\n * repository already has is left alone, with the line to add printed instead.\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport { chmod, mkdir, readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, sep } from 'node:path';\n\nimport { CONFIG_FILE } from './config.js';\nimport {\n chooseBase,\n describeGitHook,\n describeGraph,\n describeSkipped,\n enabledPlugin,\n GIT_HOOK_LINE,\n gitHookNote,\n type GitHookState,\n graphReadsBriefs,\n GUARD_HOOK,\n holdsGuard,\n loadsPlugin,\n measuresArchive,\n mergeClaudeSettings,\n mergeMcp,\n mergeSpecBrief,\n mergeSpecGraph,\n missingGitHook,\n PLUGIN,\n PRE_COMMIT,\n PROTECT_RULE_FILES,\n registersServer,\n SPEC_BRIEF_CONFIGS,\n SPEC_GRAPH_CONFIGS,\n} from './configure.js';\nimport { readJsonObject as readJson, writeAtomic } from './fs.js';\nimport { hookPath, localBranches, remoteDefault } from './git.js';\nimport { claudeSettings, pluginFile } from './round.js';\nimport { runSibling } from './siblings.js';\nimport { EXIT_ERROR, EXIT_OK, json, openWorkspace, type CliIO, type Options, type Workspace } from './workspace.js';\n\n/** `skip`: a file init would write, left alone because Claude Code's plugin brings what init would add. */\nexport type Action = 'create' | 'update' | 'keep' | 'skip' | 'run' | 'advise';\n\nexport interface Step {\n readonly file: string;\n readonly action: Action;\n readonly detail: string;\n apply?: () => Promise<void>;\n}\n\ntype Json = Record<string, unknown>;\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nconst PLUGIN_DETAIL = `load spec-harness's plugin, \"${PLUGIN}\": spec-brief's archive asks it whether a signed ruling allows a protected file, and refuses the file without it`;\n\n/** What init adds to spec-brief's configuration, said once for the plan. */\nfunction specBriefDetail(plugin: boolean, base: string | null): string {\n const parts = plugin ? [PLUGIN_DETAIL] : [];\n if (base !== null) parts.push(`measure the archive from ${base}, as spec-harness does: without a base, spec-brief's archive warns that the scope went unmeasured and checks no protected file`);\n return parts.join('; ');\n}\n\n/** A path as a person finds it: from the root when it is inside it, POSIX-separated, and as it is otherwise. */\nfunction shown(root: string, path: string): string {\n const inside = relative(root, path);\n return inside.startsWith('..') || isAbsolute(inside) ? path : inside.split(sep).join('/');\n}\n\n/**\n * git's pre-commit hook, where git runs it from, and what is there: init and\n * doctor both read it. `path` is `null` when git cannot say where its hooks\n * are. `note` is what there is to say of how a hook that runs the harness\n * runs it, or `null`.\n */\nexport async function gitHook(root: string): Promise<{ readonly hook: GitHookState; readonly path: string | null; readonly note: string | null }> {\n const path = await hookPath('pre-commit', root);\n if (path === null) return { hook: { state: 'unknown', reason: 'git does not say where they are; spec-harness needs git 2.31 or later' }, path, note: null };\n const file = shown(root, path);\n if (!existsSync(path)) return { hook: { state: 'absent', file }, path, note: null };\n const text = await readFile(path, 'utf8');\n if (!text.includes('spec-harness')) return { hook: { state: 'other', file }, path, note: null };\n // git skips a hook it cannot execute, and says so only in a hint; Windows\n // has no executable bit, and runs it.\n // The Windows job holds the platform check; the sweep runs on Linux, where it is always true.\n const inert = process.platform !== 'win32' && (statSync(path).mode & 0o111) === 0;\n return { hook: { state: inert ? 'inert' : 'runs', file }, path, note: gitHookNote(text) };\n}\n\nexport async function plan(workspace: Workspace, options: Options, env: CliIO['env']): Promise<Step[]> {\n const { root } = workspace;\n const steps: Step[] = [];\n\n // The base rounds merge into, decided once: spec-harness verifies rulings\n // against the allowed signers on it, and spec-brief's archive measures the\n // round from it. A base a person wrote, in either file, is kept.\n // openWorkspace loaded this file and refused one that is not a JSON object,\n // so here it is one, or there is none.\n const harnessFile = join(root, CONFIG_FILE);\n const harness = (await readJson(harnessFile)) as Json | null;\n const namedBase = harness !== null && typeof harness['base'] === 'string' ? harness['base'] : null;\n const choice = namedBase === null ? chooseBase({ remoteDefault: await remoteDefault(root), branch: workspace.branch, branches: await localBranches(root) }) : null;\n const base = choice === null ? namedBase : choice.base;\n\n // spec-brief first: every other setting is derived from its directories.\n const briefConfig = SPEC_BRIEF_CONFIGS.find((name) => existsSync(join(root, name)));\n const brief = workspace.siblings.locate('spec-brief');\n let briefs = 'briefs';\n let archive = 'briefs/archive';\n if (briefConfig === undefined) {\n if (brief.kind === 'found') {\n const command = brief.command;\n const created = join(root, '.spec-brief.json');\n steps.push({\n file: '.spec-brief.json',\n action: 'run',\n detail: 'spec-brief init: the brief and archive directories, every default spelled out',\n apply: async () => {\n const run = await runSibling(command, ['init'], root);\n if (run.code !== 0) throw new Error(`spec-brief init failed: ${run.stderr.trim()}`);\n },\n });\n steps.push({\n file: '.spec-brief.json',\n action: 'update',\n detail: specBriefDetail(true, base),\n apply: async () => {\n const written = await readJson(created);\n if (written === null || written === 'unreadable') throw new Error('spec-brief init wrote no configuration JSON can read');\n const merged = mergeSpecBrief(written, base, pluginFile(root));\n if (merged !== null) await writeAtomic(created, stringify(merged));\n },\n });\n } else {\n steps.push({ file: '.spec-brief.json', action: 'advise', detail: brief.reason });\n }\n } else {\n const file = join(root, briefConfig);\n const config = await readJson(file);\n // The file was found a moment ago, so null is one removed in between; and\n // a string has neither key, so these checks are there for the type.\n if (config !== null && config !== 'unreadable') {\n if (typeof config['briefs'] === 'string') briefs = config['briefs'];\n archive = typeof config['archive'] === 'string' ? config['archive'] : `${briefs}/archive`;\n }\n steps.push({ file: briefConfig, action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });\n // spec-brief's archive learns that a signed ruling allows a protected file\n // only from this package's plugin (ADR-0006).\n if (config === 'unreadable' || config === null) {\n const measure = base === null ? '' : `, and \"archiving\": { \"base\": \"${base}\" }`;\n steps.push({ file: briefConfig, action: 'advise', detail: `cannot be read as JSON; add \"plugins\": [\"${PLUGIN}\"]${measure} by hand` });\n } else {\n const isPluginFile = pluginFile(root);\n const merged = mergeSpecBrief(config, base, isPluginFile);\n const measured = measuresArchive(config);\n if (merged === null) {\n const archiving = config['archiving'];\n const from = measured ? `, and the archive is measured from ${String((archiving as Json)['base'])}` : '';\n steps.push({ file: briefConfig, action: 'keep', detail: `spec-harness's plugin is loaded${from}` });\n } else {\n const detail = specBriefDetail(!loadsPlugin(config, isPluginFile), measured ? null : base);\n steps.push({ file: briefConfig, action: 'update', detail, apply: () => writeAtomic(file, stringify(merged)) });\n }\n }\n }\n\n // spec-graph: an archived brief is a record, not a retired decision. Its\n // configuration is the first of its files, then a key in package.json; a\n // file written beside that key would shadow it.\n const graph = workspace.siblings.locate('spec-graph');\n if (graph.kind === 'outdated') steps.push({ file: '.spec-graph.json', action: 'advise', detail: graph.reason });\n if (graph.kind === 'found') {\n const archiveGlob = `${archive}/**`;\n const graphName = SPEC_GRAPH_CONFIGS.find((name) => existsSync(join(root, name)));\n const manifest = graphName === undefined ? await readJson(join(root, 'package.json')) : null;\n if (manifest !== null && manifest !== 'unreadable' && 'spec-graph' in manifest) {\n steps.push({ file: 'package.json', action: 'advise', detail: `spec-graph reads its configuration from the \"spec-graph\" key here, which init does not edit: add \"${archiveGlob}\" to its \"historyPatterns\" by hand` });\n } else {\n const file = graphName ?? '.spec-graph.json';\n const current = await readJson(join(root, file));\n if (current === 'unreadable') {\n steps.push({ file, action: 'advise', detail: `cannot be read as JSON; add \"historyPatterns\": [\"${archiveGlob}\"] by hand` });\n } else {\n const merged = mergeSpecGraph(current ?? {}, archiveGlob);\n const detail = describeGraph(archiveGlob, briefs, graphReadsBriefs(current ?? {}, briefs), merged !== null);\n if (merged === null) steps.push({ file, action: 'keep', detail });\n else steps.push({ file, action: current === null ? 'create' : 'update', detail, apply: () => writeAtomic(join(root, file), stringify(merged)) });\n }\n }\n }\n\n // spec-harness's own file, naming the base the rounds merge into: every\n // ruling is verified against the allowed signers on it. No choice was made\n // only where the file names a base.\n if (choice === null) {\n steps.push({ file: CONFIG_FILE, action: 'keep', detail: `rounds are measured from ${namedBase}` });\n } else if (choice.base === null) {\n steps.push({ file: CONFIG_FILE, action: 'advise', detail: choice.detail });\n } else {\n const content: Json = { ...harness, base: choice.base };\n steps.push({ file: CONFIG_FILE, action: harness === null ? 'create' : 'update', detail: choice.detail, apply: () => writeAtomic(harnessFile, stringify(content)) });\n }\n\n // The agent's hooks and server, unless the plugin brings them: Claude Code\n // runs a plugin's hook beside the same hook in settings, so both would\n // guard every write twice (ADR-0012).\n const plugin = enabledPlugin(await claudeSettings(root, env));\n const settingsFile = join(root, '.claude', 'settings.json');\n const settings = await readJson(settingsFile);\n if (plugin !== null) {\n const present = holdsGuard(settings);\n steps.push({ file: '.claude/settings.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'hooks', present) });\n } else if (settings === 'unreadable') {\n steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });\n } else {\n const merged = mergeClaudeSettings(settings ?? {});\n if (merged === null) steps.push({ file: '.claude/settings.json', action: 'keep', detail: 'the guard hooks are installed' });\n else\n steps.push({\n file: '.claude/settings.json',\n action: settings === null ? 'create' : 'update',\n detail: 'the guard hooks, run with node from the project\\'s install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope',\n apply: () => writeAtomic(settingsFile, stringify(merged)),\n });\n }\n const mcpFile = join(root, '.mcp.json');\n const mcp = await readJson(mcpFile);\n if (plugin !== null) {\n const present = registersServer(mcp);\n steps.push({ file: '.mcp.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'server', present) });\n } else if (mcp === 'unreadable') {\n steps.push({ file: '.mcp.json', action: 'advise', detail: 'cannot be read as JSON; add the spec-harness server by hand' });\n } else {\n const merged = mergeMcp(mcp ?? {});\n if (merged === null) steps.push({ file: '.mcp.json', action: 'keep', detail: 'the spec-harness server is registered' });\n else\n steps.push({\n file: '.mcp.json',\n action: mcp === null ? 'create' : 'update',\n detail: 'the spec-harness MCP server, run with node from the project\\'s install: start_round, check_path, request_escalation, audit_round, list_rounds',\n apply: () => writeAtomic(mcpFile, stringify(merged)),\n });\n }\n\n // git's hook, where git runs it from: core.hooksPath as git reads it, and\n // a linked worktree's shared hooks. It is the guard for a write the agent's\n // hooks never see, one through a shell, so it is advised when not asked\n // for; it is configuration outside the tree, so it is written only then.\n const { hook, path: hookFile, note: hookNote } = await gitHook(root);\n // Only a git older than 2.31, which the README states as the harness's\n // floor, cannot name its hooks, and init runs only in a work tree, where\n // any later git names them: no test reaches this advice, by that decision.\n if (hook.state === 'unknown' || hookFile === null) {\n steps.push({ file: 'pre-commit', action: 'advise', detail: describeGitHook(hook) });\n } else if (hook.state === 'runs') {\n // A hook that runs it through npx is kept, as any hook that exists is,\n // with the line that runs it as init writes the hook now.\n steps.push({ file: hook.file, action: 'keep', detail: hookNote === null ? 'already runs spec-harness' : `already runs spec-harness; ${hookNote}` });\n } else if (hook.state === 'inert') {\n steps.push({ file: hook.file, action: 'advise', detail: describeGitHook(hook) });\n } else if (hook.state === 'other') {\n steps.push({ file: hook.file, action: 'advise', detail: `a pre-commit hook exists; add the line \"${GIT_HOOK_LINE}\" to it` });\n } else if (options.gitHook) {\n steps.push({\n file: hook.file,\n action: 'create',\n detail: 'refuse a commit that changes what the active brief protects',\n apply: async () => {\n await mkdir(dirname(hookFile), { recursive: true });\n await writeAtomic(hookFile, PRE_COMMIT);\n await chmod(hookFile, 0o755);\n },\n });\n } else {\n steps.push({ file: hook.file, action: 'advise', detail: missingGitHook(null) });\n }\n\n // The file the configuration names, which a ruling's signature is checked\n // against on the base branch (ADR-0006).\n steps.push({\n file: workspace.config.rulings.allowedSigners,\n action: 'advise',\n detail: `rulings count only when signed by a key listed here on the base branch: one line per person, <email> namespaces=\"git\" <public key>. Where an agent runs as the person, make it a FIDO2 key, ssh-keygen -t ed25519-sk, whose signature needs a touch no process can supply. ${PROTECT_RULE_FILES}`,\n });\n return steps;\n}\n\nexport async function initCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const steps = await plan(workspace, options, io.env);\n if (options.write) {\n for (const step of steps) {\n if (step.apply !== undefined) {\n try {\n await step.apply();\n } catch (error) {\n io.stderr.write(`spec-harness: ${step.file}: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n }\n }\n if (options.format === 'json') {\n io.stdout.write(json('init', { written: options.write, steps: steps.map(({ file, action, detail }) => ({ file, action, detail })) }));\n } else {\n for (const step of steps) io.stdout.write(`${step.action.padEnd(6)} ${step.file}\\n ${step.detail}\\n`);\n if (!options.write && steps.some((step) => step.apply !== undefined)) io.stdout.write('\\nNothing was changed. Run again with --write to apply the plan.\\n');\n }\n return EXIT_OK;\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness init`: the family configured to agree, from the first day.\n *\n * The integration faults found when the tools were first run together were all\n * configuration: spec-graph read an archived brief as a retired decision and\n * reported every brief that depended on one; spec-guard executed an archived\n * brief's assertions. Each tool can be told the truth; `init` tells each of\n * them the same truth, derived from spec-brief's own settings.\n *\n * It prints a plan and changes nothing until `--write`. It merges into files\n * that exist and never replaces what a person wrote; a git hook the\n * repository already has is left alone, with the line to add printed instead.\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport { chmod, mkdir, readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, sep } from 'node:path';\n\nimport { CONFIG_FILE } from './config.js';\nimport {\n chooseBase,\n describeAbsent,\n describeGitHook,\n describeGraph,\n describeHooksOff,\n describePresence,\n describeSkipped,\n GIT_HOOK_LINE,\n gitHookNote,\n type GitHookState,\n graphReadsBriefs,\n GUARD_HOOK,\n guardSwitchedOff,\n isStarted,\n loadsPlugin,\n measuresArchive,\n mergeClaudeSettings,\n mergeMcp,\n mergeSpecBrief,\n mergeSpecGraph,\n missingGitHook,\n patternReader,\n PLUGIN,\n PRE_COMMIT,\n PROJECT_SETTINGS,\n PROTECT_RULE_FILES,\n registersServer,\n SPEC_BRIEF_CONFIGS,\n SPEC_GRAPH_CONFIGS,\n} from './configure.js';\nimport { readJsonObject as readJson, writeAtomic } from './fs.js';\nimport { hookPath, localBranches, remoteDefault } from './git.js';\nimport { claudeCodeWiring, pluginFile } from './round.js';\nimport { runSibling } from './siblings.js';\nimport { answerOf, EXIT_ERROR, EXIT_OK, json, openWorkspace, type CliIO, type Options, type Workspace } from './workspace.js';\n\n/** `skip`: a file init would write, left alone because Claude Code's plugin brings what init would add. */\nexport type Action = 'create' | 'update' | 'keep' | 'skip' | 'run' | 'advise';\n\nexport interface Step {\n readonly file: string;\n readonly action: Action;\n readonly detail: string;\n apply?: () => Promise<void>;\n}\n\ntype Json = Record<string, unknown>;\n\n/** The project's settings file for Claude Code, as a person finds it and as doctor names it. */\nconst SETTINGS = PROJECT_SETTINGS;\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nconst PLUGIN_DETAIL = `load spec-harness's plugin, \"${PLUGIN}\": spec-brief's archive asks it whether a signed ruling allows a protected file, and refuses the file without it`;\n\n/** What init adds to spec-brief's configuration, said once for the plan. */\nfunction specBriefDetail(plugin: boolean, base: string | null): string {\n const parts = plugin ? [PLUGIN_DETAIL] : [];\n if (base !== null) parts.push(`measure the archive from ${base}, as spec-harness does: without a base, spec-brief's archive warns that the scope went unmeasured and checks no protected file`);\n return parts.join('; ');\n}\n\n/** A path as a person finds it: from the root when it is inside it, POSIX-separated, and as it is otherwise. */\nfunction shown(root: string, path: string): string {\n const inside = relative(root, path);\n return inside.startsWith('..') || isAbsolute(inside) ? path : inside.split(sep).join('/');\n}\n\n/**\n * git's pre-commit hook, where git runs it from, and what is there: init and\n * doctor both read it. `path` is `null` when git cannot say where its hooks\n * are. `note` is what there is to say of how a hook that runs the harness\n * runs it, or `null`.\n */\nexport async function gitHook(root: string): Promise<{ readonly hook: GitHookState; readonly path: string | null; readonly note: string | null }> {\n const path = await hookPath('pre-commit', root);\n if (path === null) return { hook: { state: 'unknown', reason: 'git does not say where they are; spec-harness needs git 2.31 or later' }, path, note: null };\n const file = shown(root, path);\n if (!existsSync(path)) return { hook: { state: 'absent', file }, path, note: null };\n const text = await readFile(path, 'utf8');\n if (!text.includes('spec-harness')) return { hook: { state: 'other', file }, path, note: null };\n // git skips a hook it cannot execute, and says so only in a hint; Windows\n // has no executable bit, and runs it.\n // The Windows job holds the platform check; the sweep runs on Linux, where it is always true.\n const inert = process.platform !== 'win32' && (statSync(path).mode & 0o111) === 0;\n return { hook: { state: inert ? 'inert' : 'runs', file }, path, note: gitHookNote(text) };\n}\n\nexport async function plan(workspace: Workspace, options: Options, io: CliIO): Promise<Step[]> {\n const { root } = workspace;\n const steps: Step[] = [];\n\n // The base rounds merge into, decided once: spec-harness verifies rulings\n // against the allowed signers on it, and spec-brief's archive measures the\n // round from it. A base a person wrote, in either file, is kept.\n // openWorkspace loaded this file and refused one that is not a JSON object,\n // so here it is one, or there is none.\n const harnessFile = join(root, CONFIG_FILE);\n const harness = (await readJson(harnessFile)) as Json | null;\n const namedBase = harness !== null && typeof harness['base'] === 'string' ? harness['base'] : null;\n // What the plan asks of git and of Claude Code's settings waits on the\n // root and on nothing of the rest, so it is asked at once: three of git's\n // processes side by side where each waited on the one before. Each is\n // read below where it was asked before, so a plan that fails still fails\n // on the first of them in that order. The matchers in the settings are\n // read with one reader, for the wiring and for what to write.\n const pattern = patternReader();\n const [factsAsked, wiringAsked, hookAsked] = await Promise.allSettled([\n namedBase === null ? Promise.all([remoteDefault(root), localBranches(root)]) : null,\n claudeCodeWiring(root, io, pattern),\n gitHook(root),\n ]);\n const facts = answerOf(factsAsked);\n const choice = facts === null ? null : chooseBase({ remoteDefault: facts[0], branch: workspace.branch, branches: facts[1] });\n const base = choice === null ? namedBase : choice.base;\n\n // spec-brief first: every other setting is derived from its directories.\n const briefConfig = SPEC_BRIEF_CONFIGS.find((name) => existsSync(join(root, name)));\n const brief = workspace.siblings.locate('spec-brief');\n let briefs = 'briefs';\n let archive = 'briefs/archive';\n if (briefConfig === undefined) {\n if (brief.kind === 'found') {\n const command = brief.command;\n const created = join(root, '.spec-brief.json');\n steps.push({\n file: '.spec-brief.json',\n action: 'run',\n detail: 'spec-brief init: the brief and archive directories, every default spelled out',\n apply: async () => {\n const run = await runSibling(command, ['init'], root);\n if (run.code !== 0) throw new Error(`spec-brief init failed: ${run.stderr.trim()}`);\n },\n });\n steps.push({\n file: '.spec-brief.json',\n action: 'update',\n detail: specBriefDetail(true, base),\n apply: async () => {\n const written = await readJson(created);\n if (written === null || written === 'unreadable') throw new Error('spec-brief init wrote no configuration JSON can read');\n const merged = mergeSpecBrief(written, base, pluginFile(root));\n if (merged !== null) await writeAtomic(created, stringify(merged));\n },\n });\n } else {\n steps.push({ file: '.spec-brief.json', action: 'advise', detail: brief.reason });\n }\n } else {\n const file = join(root, briefConfig);\n const config = await readJson(file);\n // The file was found a moment ago, so null is one removed in between; and\n // a string has neither key, so these checks are there for the type.\n if (config !== null && config !== 'unreadable') {\n if (typeof config['briefs'] === 'string') briefs = config['briefs'];\n archive = typeof config['archive'] === 'string' ? config['archive'] : `${briefs}/archive`;\n }\n steps.push({ file: briefConfig, action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });\n // spec-brief's archive learns that a signed ruling allows a protected file\n // only from this package's plugin (ADR-0006).\n if (config === 'unreadable' || config === null) {\n const measure = base === null ? '' : `, and \"archiving\": { \"base\": \"${base}\" }`;\n steps.push({ file: briefConfig, action: 'advise', detail: `cannot be read as JSON; add \"plugins\": [\"${PLUGIN}\"]${measure} by hand` });\n } else {\n const isPluginFile = pluginFile(root);\n const merged = mergeSpecBrief(config, base, isPluginFile);\n const measured = measuresArchive(config);\n if (merged === null) {\n const archiving = config['archiving'];\n const from = measured ? `, and the archive is measured from ${String((archiving as Json)['base'])}` : '';\n steps.push({ file: briefConfig, action: 'keep', detail: `spec-harness's plugin is loaded${from}` });\n } else {\n const detail = specBriefDetail(!loadsPlugin(config, isPluginFile), measured ? null : base);\n steps.push({ file: briefConfig, action: 'update', detail, apply: () => writeAtomic(file, stringify(merged)) });\n }\n }\n }\n\n // spec-graph: an archived brief is a record, not a retired decision. Its\n // configuration is the first of its files, then a key in package.json; a\n // file written beside that key would shadow it.\n const graph = workspace.siblings.locate('spec-graph');\n if (graph.kind === 'outdated') steps.push({ file: '.spec-graph.json', action: 'advise', detail: graph.reason });\n if (graph.kind === 'found') {\n const archiveGlob = `${archive}/**`;\n const graphName = SPEC_GRAPH_CONFIGS.find((name) => existsSync(join(root, name)));\n const manifest = graphName === undefined ? await readJson(join(root, 'package.json')) : null;\n if (manifest !== null && manifest !== 'unreadable' && 'spec-graph' in manifest) {\n steps.push({ file: 'package.json', action: 'advise', detail: `spec-graph reads its configuration from the \"spec-graph\" key here, which init does not edit: add \"${archiveGlob}\" to its \"historyPatterns\" by hand` });\n } else {\n const file = graphName ?? '.spec-graph.json';\n const current = await readJson(join(root, file));\n if (current === 'unreadable') {\n steps.push({ file, action: 'advise', detail: `cannot be read as JSON; add \"historyPatterns\": [\"${archiveGlob}\"] by hand` });\n } else {\n const merged = mergeSpecGraph(current ?? {}, archiveGlob);\n const detail = describeGraph(archiveGlob, briefs, graphReadsBriefs(current ?? {}, briefs), merged !== null);\n if (merged === null) steps.push({ file, action: 'keep', detail });\n else steps.push({ file, action: current === null ? 'create' : 'update', detail, apply: () => writeAtomic(join(root, file), stringify(merged)) });\n }\n }\n }\n\n // spec-harness's own file, naming the base the rounds merge into: every\n // ruling is verified against the allowed signers on it. No choice was made\n // only where the file names a base.\n if (choice === null) {\n steps.push({ file: CONFIG_FILE, action: 'keep', detail: `rounds are measured from ${namedBase}` });\n } else if (choice.base === null) {\n steps.push({ file: CONFIG_FILE, action: 'advise', detail: choice.detail });\n } else {\n const content: Json = { ...harness, base: choice.base };\n steps.push({ file: CONFIG_FILE, action: harness === null ? 'create' : 'update', detail: choice.detail, apply: () => writeAtomic(harnessFile, stringify(content)) });\n }\n\n // The agent's hooks and server, unless the plugin brings them: Claude Code\n // runs a plugin's hook beside the same hook in settings, so both would\n // guard every write twice (ADR-0012). The hook's lines are known as doctor\n // knows them, by the hook's own reading and by the writes Claude Code\n // starts each for, so that the two say the same of every file (ADR-0005).\n const wiring = answerOf(wiringAsked);\n const { plugin } = wiring;\n // A plugin that is not on this machine brings neither. init writes none\n // on that reading alone, which a plugin that is there after all would\n // make a second guard: it says what doctor says, and how to have the\n // entries here instead.\n const absent = wiring.presence?.state === 'absent' ? wiring.presence.detail : null;\n const settingsFile = join(root, '.claude', 'settings.json');\n const settings = await readJson(settingsFile);\n const lines = wiring.lines.filter((line) => line.file === SETTINGS);\n // What doctor says of a line this file holds is said here too, with why\n // init leaves the line: one a person changed is theirs, and a second line\n // beside one that is started would run for every write as well.\n const said = lines.flatMap((line) => [line.detail, line.cover]).filter((note): note is string => note !== null);\n const left = said.length === 0 ? '' : `${said.join('; ')}; init rewrites no line of the hook it did not write, and writes none beside one Claude Code starts for a write`;\n if (plugin !== null) {\n // Doubled in this file or in one of the person's own, it is the double\n // install doctor fails: said here too, though init writes neither file.\n // This file's own entry is said first, so the others are named only\n // where it holds none.\n const present = wiring.hooks.includes(SETTINGS);\n if (absent !== null) steps.push({ file: SETTINGS, action: 'advise', detail: describeAbsent(plugin, absent, 'hooks', present) });\n else {\n // A plugin that cannot be told to be on this machine is no reason to\n // write either: said beside the reason, as doctor says it.\n const untold = describePresence(wiring);\n const detail = describeSkipped(plugin, 'hooks', present, wiring.hooks);\n steps.push({ file: SETTINGS, action: wiring.hooks.length > 0 ? 'advise' : 'skip', detail: untold === null ? detail : `${detail}. ${untold}` });\n }\n } else if (settings === 'unreadable') {\n steps.push({ file: SETTINGS, action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });\n } else {\n // An entry is a line of the hook where doctor reports its words as one in this file.\n const reported = new Set(lines.map((line) => JSON.stringify(line.line)));\n const merged = mergeClaudeSettings(settings ?? {}, (words) => reported.has(JSON.stringify(words)), pattern);\n // A line that turns the guard off or holds every write is one doctor\n // fails, and lines of which none is waited on before a write are no\n // guard between them: left as they are, they are for the person to mend.\n const broken = lines.some((line) => line.state === 'off' || line.state === 'held') || !lines.some((line) => isStarted(line.before));\n if (merged === null) steps.push({ file: SETTINGS, action: broken ? 'advise' : 'keep', detail: left === '' ? 'the guard hooks are installed' : left });\n else\n steps.push({\n file: SETTINGS,\n action: settings === null ? 'create' : 'update',\n detail: `the guard hooks, run with node from the project's install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope${left === '' ? '' : `; ${left}`}`,\n apply: () => writeAtomic(settingsFile, stringify(merged)),\n });\n }\n // disableAllHooks turns off whatever hooks there are, and those init\n // writes with them: said of the file that holds the key, which is the\n // person's to change. init writes what it would have written.\n if (wiring.hooksOff !== null) steps.push({ file: wiring.hooksOff, action: 'advise', detail: describeHooksOff(wiring.hooksOff, guardSwitchedOff(wiring)) });\n const mcpFile = join(root, '.mcp.json');\n const mcp = await readJson(mcpFile);\n if (plugin !== null) {\n const present = registersServer(mcp);\n if (absent !== null) steps.push({ file: '.mcp.json', action: 'advise', detail: describeAbsent(plugin, absent, 'server', present) });\n else steps.push({ file: '.mcp.json', action: present ? 'advise' : 'skip', detail: describeSkipped(plugin, 'server', present) });\n } else if (mcp === 'unreadable') {\n steps.push({ file: '.mcp.json', action: 'advise', detail: 'cannot be read as JSON; add the spec-harness server by hand' });\n } else {\n const merged = mergeMcp(mcp ?? {});\n if (merged === null) steps.push({ file: '.mcp.json', action: 'keep', detail: 'the spec-harness server is registered' });\n else\n steps.push({\n file: '.mcp.json',\n action: mcp === null ? 'create' : 'update',\n detail: 'the spec-harness MCP server, run with node from the project\\'s install: start_round, check_path, request_escalation, audit_round, list_rounds',\n apply: () => writeAtomic(mcpFile, stringify(merged)),\n });\n }\n\n // git's hook, where git runs it from: core.hooksPath as git reads it, and\n // a linked worktree's shared hooks. It is the guard for a write the agent's\n // hooks never see, one through a shell, so it is advised when not asked\n // for; it is configuration outside the tree, so it is written only then.\n const { hook, path: hookFile, note: hookNote } = answerOf(hookAsked);\n // Only a git older than 2.31, which the README states as the harness's\n // floor, cannot name its hooks, and init runs only in a work tree, where\n // any later git names them: no test reaches this advice, by that decision.\n if (hook.state === 'unknown' || hookFile === null) {\n steps.push({ file: 'pre-commit', action: 'advise', detail: describeGitHook(hook) });\n } else if (hook.state === 'runs') {\n // A hook that runs it through npx is kept, as any hook that exists is,\n // with the line that runs it as init writes the hook now.\n steps.push({ file: hook.file, action: 'keep', detail: hookNote === null ? 'already runs spec-harness' : `already runs spec-harness; ${hookNote}` });\n } else if (hook.state === 'inert') {\n steps.push({ file: hook.file, action: 'advise', detail: describeGitHook(hook) });\n } else if (hook.state === 'other') {\n steps.push({ file: hook.file, action: 'advise', detail: `a pre-commit hook exists; add the line \"${GIT_HOOK_LINE}\" to it` });\n } else if (options.gitHook) {\n steps.push({\n file: hook.file,\n action: 'create',\n detail: 'refuse a commit that changes what the active brief protects',\n apply: async () => {\n await mkdir(dirname(hookFile), { recursive: true });\n await writeAtomic(hookFile, PRE_COMMIT);\n await chmod(hookFile, 0o755);\n },\n });\n } else {\n steps.push({ file: hook.file, action: 'advise', detail: missingGitHook(null) });\n }\n\n // The file the configuration names, which a ruling's signature is checked\n // against on the base branch (ADR-0006).\n steps.push({\n file: workspace.config.rulings.allowedSigners,\n action: 'advise',\n detail: `rulings count only when signed by a key listed here on the base branch: one line per person, <email> namespaces=\"git\" <public key>. Where an agent runs as the person, make it a FIDO2 key, ssh-keygen -t ed25519-sk, whose signature needs a touch no process can supply. ${PROTECT_RULE_FILES}`,\n });\n return steps;\n}\n\nexport async function initCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const steps = await plan(workspace, options, io);\n if (options.write) {\n for (const step of steps) {\n if (step.apply !== undefined) {\n try {\n await step.apply();\n } catch (error) {\n io.stderr.write(`spec-harness: ${step.file}: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n }\n }\n if (options.format === 'json') {\n io.stdout.write(json('init', { written: options.write, steps: steps.map(({ file, action, detail }) => ({ file, action, detail })) }));\n } else {\n for (const step of steps) io.stdout.write(`${step.action.padEnd(6)} ${step.file}\\n ${step.detail}\\n`);\n if (!options.write && steps.some((step) => step.apply !== undefined)) io.stdout.write('\\nNothing was changed. Run again with --write to apply the plan.\\n');\n }\n return EXIT_OK;\n}\n"]}
package/dist/signers.js CHANGED
@@ -24,7 +24,11 @@ export const FIDO2_KEY_TYPES = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nis
24
24
  * type OpenSSH adds later is read as one, as OpenSSH reads it.
25
25
  */
26
26
  const KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;
27
- /** What OpenSSH's `strdelim` skips as whitespace between fields. */
27
+ /**
28
+ * What OpenSSH's `strdelim` skips as whitespace between fields. Nothing is a
29
+ * match too, at the start of any text, so the anchor is equivalent to its
30
+ * mutant.
31
+ */
28
32
  const WHITESPACE = /^[ \t\r\n]*/;
29
33
  /**
30
34
  * The principals field, as OpenSSH's `strdelimw` reads it: up to the first
@@ -1 +1 @@
1
- {"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,yGAAyG;AACzG,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,4BAA4B,EAAE,oCAAoC,CAAC,CAAC;AAsBvH;;;;;;GAMG;AACH,MAAM,GAAG,GAAG,4BAA4B,CAAC;AAEzC,oEAAoE;AACpE,MAAM,UAAU,GAAG,aAAa,CAAC;AAEjC;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAChD,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,GAAG;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC;IAC/G,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC;AACvH,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,0EAA0E;IAC1E,sDAAsD;IACtD,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG;YAAE,CAAC,IAAI,CAAC,CAAC;aAC7D,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,MAAM,GAAG,CAAC,MAAM,CAAC;IACpD,CAAC;IACD,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,wDAAwD;AACxD,SAAS,IAAI,CAAC,IAAY;IACxB,0EAA0E;IAC1E,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAE,CAAC;IAC7D,OAAO,EAAE,KAAK,EAAE,KAAe,EAAE,IAAI,EAAE,IAAc,EAAE,CAAC;AAC1D,CAAC;AASD,MAAM,UAAU,GAAY,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AAE5G,kEAAkE;AAClE,MAAM,MAAM,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;AAE7D;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,wEAAwE;QACxE,mEAAmE;QACnE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC;QACvC,IAAI,GAAG,KAAK,gBAAgB,IAAI,CAAC,MAAM,EAAE,CAAC;YACxC,aAAa,GAAG,IAAI,CAAC;QACvB,CAAC;aAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;YAC1C,mEAAmE;YACnE,uBAAuB;YACvB,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;YAC7D,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,IAAI,qDAAqD,IAAI,QAAQ,EAAE,CAAC;YAC/H,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,+BAA+B,EAAE,CAAC;YACxF,MAAM,CAAC,GAAG,CAAC,GAAG,EAAG,MAAM,CAAC,CAAC,CAAY,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;YAC5D,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,OAAO,EAAE,OAAO,EAAE,0GAA0G,EAAE,CAAC;QACjI,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,GAAG;YAAE,OAAO,EAAE,OAAO,EAAE,yCAAyC,EAAE,CAAC;QAC3F,EAAE,IAAI,CAAC,CAAC;QACR,IAAI,EAAE,KAAK,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,EAAE,CAAC;AACzK,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAwC,EAAE,CAAC;IACzD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QACzC,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;QACvB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO;QACtD,MAAM,MAAM,GAAG,CAAC,OAAe,EAAQ,EAAE;YACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACnC,CAAC,CAAC;QACF,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC,yDAAyD,CAAC,CAAC;QAClG,uEAAuE;QACvE,gEAAgE;QAChE,IAAI,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,MAAM,CAAC,sDAAsD,CAAC,CAAC;YAC1F,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC;YACtB,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;QAClC,oEAAoE;QACpE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC,+DAA+D,CAAC,CAAC;QACnG,kFAAkF;QAClF,MAAM,OAAO,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACrE,IAAI,SAAS,IAAI,OAAO;YAAE,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI;YACJ,oEAAoE;YACpE,qCAAqC;YACrC,UAAU,EAAE,UAAU,CAAC,KAAK;iBACzB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;iBACpC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,KAAK,EAAE,CAAC;YAC1C,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,UAAU,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACzG,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,OAAO,EAAE,IAAI,CAAC,KAAK;YACnB,GAAG;SACJ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAiC;IACxD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,aAAa,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AACxG,CAAC","sourcesContent":["/**\n * The allowed-signers file, read as OpenSSH reads it (ssh-keygen(1), ALLOWED\n * SIGNERS), to say which signers sign with a key that is not FIDO2.\n *\n * A line is `principals [options] keytype key [comment]`: principals a\n * comma-separated list, which may hold a double-quoted part; options a\n * comma-separated list with no space outside quotes, such as\n * `cert-authority`, `namespaces=\"git\"` or `valid-after=\"20260101\"`, each\n * value in double quotes. The quotes are read as OpenSSH's sshsig.c and\n * misc.c read them, and each function below names the one it follows. A\n * `cert-authority` line holds the key of a certificate authority, not a\n * person's: the keys it certifies are not in the file, so their type cannot\n * be read from it.\n *\n * Pure: doctor reads the file from the base and passes the text.\n */\n\n/** The key types of a FIDO2 security key: a signature needs a touch no process can supply (ADR-0006). */\nexport const FIDO2_KEY_TYPES: readonly string[] = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com'];\n\nexport interface AllowedSigner {\n /** 1-based. */\n readonly line: number;\n readonly principals: readonly string[];\n /** Whether the key is a certificate authority's. */\n readonly certAuthority: boolean;\n /** The namespaces the key may sign in, or `null` for any. */\n readonly namespaces: readonly string[] | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n readonly keyType: string;\n readonly key: string;\n}\n\nexport interface SignersRead {\n readonly signers: readonly AllowedSigner[];\n /** Lines that are not a signer, with why. */\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n}\n\n/**\n * A key as the file writes it: base64 of the key's wire form, which starts\n * with the four-byte length of its type's name, so with `AAAA`. OpenSSH tells\n * options from a key type by trying to read a key after it; a key type is\n * followed by a key, and options by a key type, which is not one. So a key\n * type OpenSSH adds later is read as one, as OpenSSH reads it.\n */\nconst KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;\n\n/** What OpenSSH's `strdelim` skips as whitespace between fields. */\nconst WHITESPACE = /^[ \\t\\r\\n]*/;\n\n/**\n * The principals field, as OpenSSH's `strdelimw` reads it: up to the first\n * whitespace or double quote. A quote there, at the start or in the middle,\n * is taken out with the next one, and the field ends at that one whatever\n * follows it: `a@example.com,\"b@example.com\"` is\n * `a@example.com,b@example.com`, and `\"a\"b` is `a`, with `b` the next field.\n * `null` when the quote is never closed, which makes the line no signer.\n */\nfunction principalsField(text: string): { value: string; rest: string } | null {\n const at = text.search(/[ \\t\\r\\n\"]/);\n if (at === -1) return { value: text, rest: '' };\n if (text.charAt(at) !== '\"') return { value: text.slice(0, at), rest: text.slice(at).replace(WHITESPACE, '') };\n const close = text.indexOf('\"', at + 1);\n if (close === -1) return null;\n return { value: text.slice(0, at) + text.slice(at + 1, close), rest: text.slice(close + 1).replace(WHITESPACE, '') };\n}\n\n/**\n * The options field, as OpenSSH's `sshkey_advance_past_options` finds its\n * end: the first space or tab outside double quotes, where `\\\"` is a quote\n * inside them. `null` when a quote is never closed.\n */\nfunction optionsField(text: string): { value: string; rest: string } | null {\n // Reading one character past the end changes nothing - `charAt` gives the\n // empty string there - so `<=` for `<` is equivalent.\n let quoted = false;\n let i = 0;\n for (; i < text.length && (quoted || (text.charAt(i) !== ' ' && text.charAt(i) !== '\\t')); i += 1) {\n if (text.charAt(i) === '\\\\' && text.charAt(i + 1) === '\"') i += 1;\n else if (text.charAt(i) === '\"') quoted = !quoted;\n }\n if (quoted) return null;\n return { value: text.slice(0, i), rest: text.slice(i).replace(/^[ \\t]*/, '') };\n}\n\n/** A key type or a key: up to the next space or tab. */\nfunction word(text: string): { value: string; rest: string } {\n // Each part of the pattern can match nothing, so it matches any text from\n // its start, and its last part runs to the end: the match is never null,\n // and either anchor is equivalent to its mutant.\n const [, value, rest] = /^([^ \\t]*)[ \\t]*(.*)$/s.exec(text)!;\n return { value: value as string, rest: rest as string };\n}\n\ninterface Options {\n readonly certAuthority: boolean;\n readonly namespaces: string | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n}\n\nconst NO_OPTIONS: Options = { certAuthority: false, namespaces: null, validAfter: null, validBefore: null };\n\n/** The options with a value, each of which OpenSSH takes once. */\nconst VALUED = ['namespaces', 'valid-after', 'valid-before'];\n\n/**\n * The options, as OpenSSH's `sshsigopt_parse` reads them: `cert-authority`\n * alone and `namespaces`, `valid-after` and `valid-before` with a value,\n * separated by commas, a name in any case and a value always in double\n * quotes, `\\\"` a quote inside one (its `opt_dequote`). Anything else - another\n * option, a value without its quotes or given twice, anything but a comma\n * after an option, a comma with nothing after it - makes the line no signer,\n * as it does for OpenSSH. The times are read as written.\n */\nfunction readOptions(text: string): Options | { refused: string } {\n const values = new Map<string, string>();\n let certAuthority = false;\n let at = 0;\n while (at < text.length) {\n // Nothing is a match too, so there is one at the start of any text: the\n // match is never null, and the anchor is equivalent to its mutant.\n const name = /^[^,=\"]*/.exec(text.slice(at))![0];\n const key = name.toLowerCase();\n at += name.length;\n const valued = text.charAt(at) === '=';\n if (key === 'cert-authority' && !valued) {\n certAuthority = true;\n } else if (VALUED.includes(key) && valued) {\n // `optionsField` has refused a quote never closed, so a value that\n // opens one closes it.\n const quoted = /^\"((?:\\\\\"|[^\"])*)\"/.exec(text.slice(at + 1));\n if (quoted === null) return { refused: `the value of ${name} is not in double quotes, as OpenSSH needs: write ${name}=\"...\"` };\n if (values.has(key)) return { refused: `it gives ${key} twice, which OpenSSH refuses` };\n values.set(key, (quoted[1] as string).replace(/\\\\\"/g, '\"'));\n at += 1 + quoted[0].length;\n } else {\n return { refused: 'OpenSSH reads only cert-authority, namespaces=\"...\", valid-after=\"...\" and valid-before=\"...\" as options' };\n }\n if (at === text.length) break;\n if (text.charAt(at) !== ',') return { refused: 'its options are not separated by commas' };\n at += 1;\n if (at === text.length) return { refused: 'its options end in a comma' };\n }\n return { certAuthority, namespaces: values.get('namespaces') ?? null, validAfter: values.get('valid-after') ?? null, validBefore: values.get('valid-before') ?? null };\n}\n\n/** Reads the file's text: every signer, and every line that is not one. */\nexport function readAllowedSigners(text: string): SignersRead {\n const signers: AllowedSigner[] = [];\n const problems: { line: number; message: string }[] = [];\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = index + 1;\n const content = raw.trim();\n if (content === '' || content.startsWith('#')) return;\n const refuse = (message: string): void => {\n problems.push({ line, message });\n };\n const principals = principalsField(content);\n if (principals === null) return refuse('its principals open a double quote that is never closed');\n // OpenSSH reads a key where the key type would be, and options when it\n // cannot: a key type is followed by a key, and options are not.\n let type = word(principals.rest);\n let written: string | null = null;\n if (!KEY.test(word(type.rest).value)) {\n const field = optionsField(principals.rest);\n if (field === null) return refuse('its options open a double quote that is never closed');\n written = field.value;\n type = word(field.rest);\n }\n const key = word(type.rest).value;\n // With no key type, nothing follows it either, and no key is a key.\n if (!KEY.test(key)) return refuse('a signer is its principals, any options, a key type and a key');\n // OpenSSH reads the key before the options, and says first what is wrong with it.\n const options = written === null ? NO_OPTIONS : readOptions(written);\n if ('refused' in options) return refuse(options.refused);\n signers.push({\n line,\n // The field keeps no quote, so every comma parts two principals, as\n // OpenSSH's pattern lists read them.\n principals: principals.value\n .split(',')\n .map((principal) => principal.trim())\n .filter((principal) => principal !== ''),\n certAuthority: options.certAuthority,\n namespaces: options.namespaces === null ? null : options.namespaces.split(',').map((name) => name.trim()),\n validAfter: options.validAfter,\n validBefore: options.validBefore,\n keyType: type.value,\n key,\n });\n });\n return { signers, problems };\n}\n\n/**\n * The signers whose own key is not a FIDO2 security key's. A certificate\n * authority's line is left out: the keys it vouches for are not in the file.\n */\nexport function notFido2(signers: readonly AllowedSigner[]): AllowedSigner[] {\n return signers.filter((signer) => !signer.certAuthority && !FIDO2_KEY_TYPES.includes(signer.keyType));\n}\n"]}
1
+ {"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The allowed-signers file, read as OpenSSH reads it (ssh-keygen(1), ALLOWED\n * SIGNERS), to say which signers sign with a key that is not FIDO2.\n *\n * A line is `principals [options] keytype key [comment]`: principals a\n * comma-separated list, which may hold a double-quoted part; options a\n * comma-separated list with no space outside quotes, such as\n * `cert-authority`, `namespaces=\"git\"` or `valid-after=\"20260101\"`, each\n * value in double quotes. The quotes are read as OpenSSH's sshsig.c and\n * misc.c read them, and each function below names the one it follows. A\n * `cert-authority` line holds the key of a certificate authority, not a\n * person's: the keys it certifies are not in the file, so their type cannot\n * be read from it.\n *\n * Pure: doctor reads the file from the base and passes the text.\n */\n\n/** The key types of a FIDO2 security key: a signature needs a touch no process can supply (ADR-0006). */\nexport const FIDO2_KEY_TYPES: readonly string[] = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com'];\n\nexport interface AllowedSigner {\n /** 1-based. */\n readonly line: number;\n readonly principals: readonly string[];\n /** Whether the key is a certificate authority's. */\n readonly certAuthority: boolean;\n /** The namespaces the key may sign in, or `null` for any. */\n readonly namespaces: readonly string[] | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n readonly keyType: string;\n readonly key: string;\n}\n\nexport interface SignersRead {\n readonly signers: readonly AllowedSigner[];\n /** Lines that are not a signer, with why. */\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n}\n\n/**\n * A key as the file writes it: base64 of the key's wire form, which starts\n * with the four-byte length of its type's name, so with `AAAA`. OpenSSH tells\n * options from a key type by trying to read a key after it; a key type is\n * followed by a key, and options by a key type, which is not one. So a key\n * type OpenSSH adds later is read as one, as OpenSSH reads it.\n */\nconst KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;\n\n/**\n * What OpenSSH's `strdelim` skips as whitespace between fields. Nothing is a\n * match too, at the start of any text, so the anchor is equivalent to its\n * mutant.\n */\nconst WHITESPACE = /^[ \\t\\r\\n]*/;\n\n/**\n * The principals field, as OpenSSH's `strdelimw` reads it: up to the first\n * whitespace or double quote. A quote there, at the start or in the middle,\n * is taken out with the next one, and the field ends at that one whatever\n * follows it: `a@example.com,\"b@example.com\"` is\n * `a@example.com,b@example.com`, and `\"a\"b` is `a`, with `b` the next field.\n * `null` when the quote is never closed, which makes the line no signer.\n */\nfunction principalsField(text: string): { value: string; rest: string } | null {\n const at = text.search(/[ \\t\\r\\n\"]/);\n if (at === -1) return { value: text, rest: '' };\n if (text.charAt(at) !== '\"') return { value: text.slice(0, at), rest: text.slice(at).replace(WHITESPACE, '') };\n const close = text.indexOf('\"', at + 1);\n if (close === -1) return null;\n return { value: text.slice(0, at) + text.slice(at + 1, close), rest: text.slice(close + 1).replace(WHITESPACE, '') };\n}\n\n/**\n * The options field, as OpenSSH's `sshkey_advance_past_options` finds its\n * end: the first space or tab outside double quotes, where `\\\"` is a quote\n * inside them. `null` when a quote is never closed.\n */\nfunction optionsField(text: string): { value: string; rest: string } | null {\n // Reading one character past the end changes nothing - `charAt` gives the\n // empty string there - so `<=` for `<` is equivalent.\n let quoted = false;\n let i = 0;\n for (; i < text.length && (quoted || (text.charAt(i) !== ' ' && text.charAt(i) !== '\\t')); i += 1) {\n if (text.charAt(i) === '\\\\' && text.charAt(i + 1) === '\"') i += 1;\n else if (text.charAt(i) === '\"') quoted = !quoted;\n }\n if (quoted) return null;\n return { value: text.slice(0, i), rest: text.slice(i).replace(/^[ \\t]*/, '') };\n}\n\n/** A key type or a key: up to the next space or tab. */\nfunction word(text: string): { value: string; rest: string } {\n // Each part of the pattern can match nothing, so it matches any text from\n // its start, and its last part runs to the end: the match is never null,\n // and either anchor is equivalent to its mutant.\n const [, value, rest] = /^([^ \\t]*)[ \\t]*(.*)$/s.exec(text)!;\n return { value: value as string, rest: rest as string };\n}\n\ninterface Options {\n readonly certAuthority: boolean;\n readonly namespaces: string | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n}\n\nconst NO_OPTIONS: Options = { certAuthority: false, namespaces: null, validAfter: null, validBefore: null };\n\n/** The options with a value, each of which OpenSSH takes once. */\nconst VALUED = ['namespaces', 'valid-after', 'valid-before'];\n\n/**\n * The options, as OpenSSH's `sshsigopt_parse` reads them: `cert-authority`\n * alone and `namespaces`, `valid-after` and `valid-before` with a value,\n * separated by commas, a name in any case and a value always in double\n * quotes, `\\\"` a quote inside one (its `opt_dequote`). Anything else - another\n * option, a value without its quotes or given twice, anything but a comma\n * after an option, a comma with nothing after it - makes the line no signer,\n * as it does for OpenSSH. The times are read as written.\n */\nfunction readOptions(text: string): Options | { refused: string } {\n const values = new Map<string, string>();\n let certAuthority = false;\n let at = 0;\n while (at < text.length) {\n // Nothing is a match too, so there is one at the start of any text: the\n // match is never null, and the anchor is equivalent to its mutant.\n const name = /^[^,=\"]*/.exec(text.slice(at))![0];\n const key = name.toLowerCase();\n at += name.length;\n const valued = text.charAt(at) === '=';\n if (key === 'cert-authority' && !valued) {\n certAuthority = true;\n } else if (VALUED.includes(key) && valued) {\n // `optionsField` has refused a quote never closed, so a value that\n // opens one closes it.\n const quoted = /^\"((?:\\\\\"|[^\"])*)\"/.exec(text.slice(at + 1));\n if (quoted === null) return { refused: `the value of ${name} is not in double quotes, as OpenSSH needs: write ${name}=\"...\"` };\n if (values.has(key)) return { refused: `it gives ${key} twice, which OpenSSH refuses` };\n values.set(key, (quoted[1] as string).replace(/\\\\\"/g, '\"'));\n at += 1 + quoted[0].length;\n } else {\n return { refused: 'OpenSSH reads only cert-authority, namespaces=\"...\", valid-after=\"...\" and valid-before=\"...\" as options' };\n }\n if (at === text.length) break;\n if (text.charAt(at) !== ',') return { refused: 'its options are not separated by commas' };\n at += 1;\n if (at === text.length) return { refused: 'its options end in a comma' };\n }\n return { certAuthority, namespaces: values.get('namespaces') ?? null, validAfter: values.get('valid-after') ?? null, validBefore: values.get('valid-before') ?? null };\n}\n\n/** Reads the file's text: every signer, and every line that is not one. */\nexport function readAllowedSigners(text: string): SignersRead {\n const signers: AllowedSigner[] = [];\n const problems: { line: number; message: string }[] = [];\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = index + 1;\n const content = raw.trim();\n if (content === '' || content.startsWith('#')) return;\n const refuse = (message: string): void => {\n problems.push({ line, message });\n };\n const principals = principalsField(content);\n if (principals === null) return refuse('its principals open a double quote that is never closed');\n // OpenSSH reads a key where the key type would be, and options when it\n // cannot: a key type is followed by a key, and options are not.\n let type = word(principals.rest);\n let written: string | null = null;\n if (!KEY.test(word(type.rest).value)) {\n const field = optionsField(principals.rest);\n if (field === null) return refuse('its options open a double quote that is never closed');\n written = field.value;\n type = word(field.rest);\n }\n const key = word(type.rest).value;\n // With no key type, nothing follows it either, and no key is a key.\n if (!KEY.test(key)) return refuse('a signer is its principals, any options, a key type and a key');\n // OpenSSH reads the key before the options, and says first what is wrong with it.\n const options = written === null ? NO_OPTIONS : readOptions(written);\n if ('refused' in options) return refuse(options.refused);\n signers.push({\n line,\n // The field keeps no quote, so every comma parts two principals, as\n // OpenSSH's pattern lists read them.\n principals: principals.value\n .split(',')\n .map((principal) => principal.trim())\n .filter((principal) => principal !== ''),\n certAuthority: options.certAuthority,\n namespaces: options.namespaces === null ? null : options.namespaces.split(',').map((name) => name.trim()),\n validAfter: options.validAfter,\n validBefore: options.validBefore,\n keyType: type.value,\n key,\n });\n });\n return { signers, problems };\n}\n\n/**\n * The signers whose own key is not a FIDO2 security key's. A certificate\n * authority's line is left out: the keys it vouches for are not in the file.\n */\nexport function notFido2(signers: readonly AllowedSigner[]): AllowedSigner[] {\n return signers.filter((signer) => !signer.certAuthority && !FIDO2_KEY_TYPES.includes(signer.keyType));\n}\n"]}
package/dist/usage.d.ts CHANGED
@@ -229,20 +229,47 @@ export interface LineFault {
229
229
  }
230
230
  /**
231
231
  * What Claude Code's hook does with its own line, as far as the line decides
232
- * it: it guards, in the root the line gives it where it gives one, or the
233
- * guard is off, for a fault the hook says with exit 1 and holds no write for
234
- * (ADR-0005). The hook reads its line through the three functions below, and
235
- * `doctor` reads a line it finds in Claude Code's settings through the same
236
- * three, so what it says of a line is what the hook does with it.
232
+ * it: it guards, in the root the line gives it where it gives one and under
233
+ * the brief it names where it names one; or the guard is off, for a fault
234
+ * the hook says with exit 1 and holds no write for; or every write is held,
235
+ * exit 2, for a brief the line names that is not one (ADR-0005). The hook
236
+ * reads its line through the functions below, and `doctor` reads a line it
237
+ * finds in Claude Code's settings through the same ones, so what it says of
238
+ * a line is what the hook does with it.
237
239
  */
238
- export type HookLine = {
240
+ export type HookLine = Guards | GuardOff | WritesHeld;
241
+ export interface Guards {
239
242
  readonly state: 'guards';
240
243
  readonly root: string | undefined;
241
- } | GuardOff;
244
+ /**
245
+ * The brief the line names with `--brief`, as written. Whether it is a
246
+ * brief is spec-brief's to say, which the caller asks. Absent where the
247
+ * line names none.
248
+ */
249
+ readonly brief?: string;
250
+ }
242
251
  export interface GuardOff {
243
252
  readonly state: 'off';
244
253
  readonly fault: LineFault;
254
+ /**
255
+ * Set where the hook says nothing of the fault and exits 0, so that no
256
+ * run of it fails: a root outside any work tree. Every other fault it
257
+ * says, with exit 1.
258
+ */
259
+ readonly silent?: true;
260
+ }
261
+ export interface WritesHeld {
262
+ readonly state: 'held';
263
+ /** What is said of the name, by what named the brief (workspace.ts). */
264
+ readonly wrong: string;
245
265
  }
266
+ /**
267
+ * A line under which the round has no brief to be checked against, since
268
+ * what names one names a brief spec-brief does not know, or one it has
269
+ * archived: the hook holds the write, exit 2, and every write after it until
270
+ * the name is mended (ADR-0004). `wrong` is what is said of the name.
271
+ */
272
+ export declare function heldHookLine(wrong: string): WritesHeld;
246
273
  /**
247
274
  * A line the parser refused: an option no command has, an option left
248
275
  * without its value, a `--format` or an `--at` that is none. The parser's
@@ -258,7 +285,16 @@ export declare function unreadHookLine(said: string): GuardOff;
258
285
  * read as a session outside a work tree, exit 0 and no word, and the guard
259
286
  * someone had configured was off.
260
287
  */
261
- export declare function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): HookLine;
288
+ export declare function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff;
289
+ /**
290
+ * A line whose root is a directory outside any git work tree. The hook
291
+ * reads that as a session outside one, where it has nothing to guard: it
292
+ * checks no write and says nothing, exit 0 (ADR-0005), and that stays. Only
293
+ * git can tell such a root, so the caller asks it. A line that carries one
294
+ * is a guard someone set up that checks nothing, and `doctor` says it as
295
+ * off.
296
+ */
297
+ export declare function outsideHookLine(root: string): GuardOff;
262
298
  /**
263
299
  * What Claude Code's hook does with the words it is handed, or `null` for
264
300
  * words that are not its line: read by the parser the command line reads
@@ -266,6 +302,10 @@ export declare function readHookLine(root: string | undefined, cwd: string, isDi
266
302
  * line that parsed and on one that did not, as the command line knows it
267
303
  * (cli.ts). `parse` is that parser, which throws its refusal with the words
268
304
  * it could still read.
305
+ *
306
+ * A line that guards says which brief it names, where it names one: the hook
307
+ * passes over a `--brief` that is empty, as every release has, and holds
308
+ * every write under one that names no brief, which only spec-brief can tell.
269
309
  */
270
310
  export declare function claudeHookLine(words: readonly string[], parse: (argv: readonly string[]) => CommandLine, cwd: string, isDirectory: (path: string) => boolean): HookLine | null;
271
311
  /**