@descent-vtt/spec-harness 0.6.0 → 0.7.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 (84) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +273 -24
  5. package/dist/audit.d.ts +71 -1
  6. package/dist/audit.js +140 -17
  7. package/dist/audit.js.map +1 -1
  8. package/dist/branch.d.ts +13 -0
  9. package/dist/branch.js +19 -0
  10. package/dist/branch.js.map +1 -1
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +51 -13
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands.js +11 -4
  15. package/dist/commands.js.map +1 -1
  16. package/dist/config.js +3 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/configure.d.ts +50 -3
  19. package/dist/configure.js +78 -5
  20. package/dist/configure.js.map +1 -1
  21. package/dist/context.d.ts +10 -5
  22. package/dist/context.js +54 -19
  23. package/dist/context.js.map +1 -1
  24. package/dist/document.d.ts +2 -1
  25. package/dist/document.js.map +1 -1
  26. package/dist/formats.d.ts +31 -0
  27. package/dist/formats.js +127 -0
  28. package/dist/formats.js.map +1 -0
  29. package/dist/git.d.ts +6 -0
  30. package/dist/git.js +8 -0
  31. package/dist/git.js.map +1 -1
  32. package/dist/guard.d.ts +8 -0
  33. package/dist/guard.js +19 -1
  34. package/dist/guard.js.map +1 -1
  35. package/dist/host.d.ts +14 -0
  36. package/dist/host.js +50 -0
  37. package/dist/host.js.map +1 -0
  38. package/dist/index.d.ts +4 -2
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/manifests.d.ts +8 -1
  42. package/dist/manifests.js +16 -6
  43. package/dist/manifests.js.map +1 -1
  44. package/dist/premises.d.ts +3 -0
  45. package/dist/premises.js +43 -13
  46. package/dist/premises.js.map +1 -1
  47. package/dist/reader.js +10 -2
  48. package/dist/reader.js.map +1 -1
  49. package/dist/round.d.ts +8 -0
  50. package/dist/round.js +73 -22
  51. package/dist/round.js.map +1 -1
  52. package/dist/server.js +9 -3
  53. package/dist/server.js.map +1 -1
  54. package/dist/setup.d.ts +10 -0
  55. package/dist/setup.js +68 -32
  56. package/dist/setup.js.map +1 -1
  57. package/dist/signers.d.ts +43 -0
  58. package/dist/signers.js +105 -0
  59. package/dist/signers.js.map +1 -0
  60. package/dist/types.d.ts +7 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
  63. package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
  64. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  65. package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
  66. package/dist/vendor/spec-core/pattern/glob.js +207 -17
  67. package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
  68. package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
  69. package/dist/vendor/spec-core/pattern/index.js +1 -1
  70. package/dist/vendor/spec-core/pattern/index.js.map +1 -1
  71. package/dist/vendor/spec-core/text/index.d.ts +1 -0
  72. package/dist/vendor/spec-core/text/index.js +1 -0
  73. package/dist/vendor/spec-core/text/index.js.map +1 -1
  74. package/dist/vendor/spec-core/text/width.d.ts +30 -0
  75. package/dist/vendor/spec-core/text/width.js +113 -0
  76. package/dist/vendor/spec-core/text/width.js.map +1 -0
  77. package/dist/versions.d.ts +27 -1
  78. package/dist/versions.js +23 -1
  79. package/dist/versions.js.map +1 -1
  80. package/dist/workspace.d.ts +6 -1
  81. package/dist/workspace.js +17 -6
  82. package/dist/workspace.js.map +1 -1
  83. package/package.json +1 -1
  84. package/skills/close-round/SKILL.md +4 -1
@@ -7,6 +7,8 @@
7
7
  * Each merge keeps what a person wrote and returns `null` when there is
8
8
  * nothing to add.
9
9
  */
10
+ import type { AllowedSigner } from './signers.js';
11
+ import { type ClaudeCodeCheck } from './versions.js';
10
12
  type Json = Record<string, unknown>;
11
13
  /** The project root, as Claude Code substitutes it in a hook's arguments and in a plugin server's. */
12
14
  export declare const PROJECT_DIR = "${CLAUDE_PROJECT_DIR}";
@@ -79,8 +81,14 @@ export declare function describeClaudeCode(wiring: ClaudeCodeWiring): string;
79
81
  export declare const PLUGIN = "@descent-vtt/spec-harness/spec-brief-plugin";
80
82
  /** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */
81
83
  export declare const SPEC_BRIEF_CONFIGS: readonly string[];
82
- /** Whether spec-brief's configuration loads the plugin, by name or as `{ module, options }`. */
83
- export declare function loadsPlugin(config: Json): boolean;
84
+ /**
85
+ * Whether spec-brief's configuration loads the plugin, by name or as
86
+ * `{ module, options }`. spec-brief also loads a plugin by a path, one that
87
+ * starts with `.` or is absolute, read from the root; such a path is the
88
+ * plugin when `isPluginFile` says it names the plugin's file, which only the
89
+ * disk can tell.
90
+ */
91
+ export declare function loadsPlugin(config: Json, isPluginFile?: (path: string) => boolean): boolean;
84
92
  /** Whether spec-brief's configuration names the base its archive measures a round from. */
85
93
  export declare function measuresArchive(config: Json): boolean;
86
94
  /**
@@ -92,7 +100,7 @@ export declare function measuresArchive(config: Json): boolean;
92
100
  * unmeasured and passes, so the gate is open until a base is named. A base a
93
101
  * person wrote is kept.
94
102
  */
95
- export declare function mergeSpecBrief(current: Json, base?: string | null): Json | null;
103
+ export declare function mergeSpecBrief(current: Json, base?: string | null, isPluginFile?: (path: string) => boolean): Json | null;
96
104
  /** Whether spec-brief loads the plugin, as its configuration at the root says. */
97
105
  export type PluginState = {
98
106
  readonly kind: 'loaded' | 'not-loaded' | 'unreadable';
@@ -138,6 +146,45 @@ export declare function describeBase(base: {
138
146
  }): string;
139
147
  /** Whether the allowed-signers file is on the base, where every ruling's signature is checked against it. */
140
148
  export declare function describeSigners(file: string, base: string | null, onBase: boolean): string;
149
+ /**
150
+ * The note doctor gives on the signers whose key is not FIDO2, or `null`
151
+ * when every one's is. A note, never a failure: ADR-0006 accepts a key the
152
+ * agent's account cannot read too, and a PIV or PKCS#11 hardware key is
153
+ * written as a plain key.
154
+ */
155
+ export declare function describeSignerKeys(signers: readonly Pick<AllowedSigner, 'line' | 'principals' | 'keyType'>[]): string | null;
156
+ /** The note doctor gives on a line of the allowed-signers file that is not a signer. */
157
+ export declare function describeSignerProblem(problem: {
158
+ readonly line: number;
159
+ readonly message: string;
160
+ }): string;
161
+ /** What doctor says of the Claude Code on `PATH`, which runs the guard's hooks only from the minimum on. */
162
+ export declare function describeClaudeRelease(check: ClaudeCodeCheck): string;
163
+ /** Where git's pre-commit hook stands: git's path for it, shown from the root, and what is there. */
164
+ export type GitHookState = {
165
+ readonly state: 'runs' | 'absent' | 'other' | 'inert';
166
+ readonly file: string;
167
+ } | {
168
+ readonly state: 'unknown';
169
+ readonly reason: string;
170
+ };
171
+ /**
172
+ * The advice for a missing hook, which init gives beside the file and doctor
173
+ * with it. The hook is the guard for a write the agent's hooks never see,
174
+ * one made through a shell (ADR-0005).
175
+ */
176
+ export declare function missingGitHook(file: string | null): string;
177
+ /**
178
+ * How to keep the files the rules live in from changing unread, on each
179
+ * forge, as spec-core's ADR-0005 asks: the forge protects them, and CI checks
180
+ * a change against the base branch's rules. GitLab Free has no Code Owners
181
+ * approval, so there the person who merges is the reviewer.
182
+ */
183
+ export declare const PROTECT_RULE_FILES: string;
184
+ /** The line to add to a hook the repository has of its own. */
185
+ export declare const GIT_HOOK_LINE = "npx --no-install spec-harness hook git";
186
+ /** What doctor says of git's pre-commit hook. */
187
+ export declare function describeGitHook(hook: GitHookState): string;
141
188
  /** spec-graph's configuration files, in the order spec-graph reads them; then a "spec-graph" key in package.json. */
142
189
  export declare const SPEC_GRAPH_CONFIGS: readonly string[];
143
190
  /** Whether spec-graph, configured as `config` says, reads the briefs in the directory `briefs`. */
package/dist/configure.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * Each merge keeps what a person wrote and returns `null` when there is
8
8
  * nothing to add.
9
9
  */
10
+ import { isAbsolute } from 'node:path';
10
11
  import { parseGlobList } from './vendor/spec-core/pattern/index.js';
12
+ import { CLAUDE_CODE_MINIMUM } from './versions.js';
11
13
  function isObject(value) {
12
14
  return typeof value === 'object' && value !== null;
13
15
  }
@@ -189,10 +191,17 @@ export function describeClaudeCode(wiring) {
189
191
  export const PLUGIN = '@descent-vtt/spec-harness/spec-brief-plugin';
190
192
  /** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */
191
193
  export const SPEC_BRIEF_CONFIGS = ['.spec-brief.json', 'spec-brief.json'];
192
- /** Whether spec-brief's configuration loads the plugin, by name or as `{ module, options }`. */
193
- export function loadsPlugin(config) {
194
+ /**
195
+ * Whether spec-brief's configuration loads the plugin, by name or as
196
+ * `{ module, options }`. spec-brief also loads a plugin by a path, one that
197
+ * starts with `.` or is absolute, read from the root; such a path is the
198
+ * plugin when `isPluginFile` says it names the plugin's file, which only the
199
+ * disk can tell.
200
+ */
201
+ export function loadsPlugin(config, isPluginFile = () => false) {
194
202
  const plugins = config['plugins'];
195
- return Array.isArray(plugins) && plugins.some((entry) => entry === PLUGIN || (isObject(entry) && entry['module'] === PLUGIN));
203
+ const names = (specifier) => specifier === PLUGIN || (typeof specifier === 'string' && (specifier.startsWith('.') || isAbsolute(specifier)) && isPluginFile(specifier));
204
+ return Array.isArray(plugins) && plugins.some((entry) => names(entry) || (isObject(entry) && names(entry['module'])));
196
205
  }
197
206
  /** Whether spec-brief's configuration names the base its archive measures a round from. */
198
207
  export function measuresArchive(config) {
@@ -208,8 +217,8 @@ export function measuresArchive(config) {
208
217
  * unmeasured and passes, so the gate is open until a base is named. A base a
209
218
  * person wrote is kept.
210
219
  */
211
- export function mergeSpecBrief(current, base = null) {
212
- const plugin = loadsPlugin(current);
220
+ export function mergeSpecBrief(current, base = null, isPluginFile) {
221
+ const plugin = loadsPlugin(current, isPluginFile);
213
222
  const measure = base !== null && !measuresArchive(current);
214
223
  if (plugin && !measure)
215
224
  return null;
@@ -276,6 +285,70 @@ export function describeSigners(file, base, onBase) {
276
285
  return `${file} is on ${base}`;
277
286
  return `${file} is not on ${base}, so no ruling can count: commit it there, one line per person: <email> namespaces="git" <public key>`;
278
287
  }
288
+ /**
289
+ * The note doctor gives on the signers whose key is not FIDO2, or `null`
290
+ * when every one's is. A note, never a failure: ADR-0006 accepts a key the
291
+ * agent's account cannot read too, and a PIV or PKCS#11 hardware key is
292
+ * written as a plain key.
293
+ */
294
+ export function describeSignerKeys(signers) {
295
+ if (signers.length === 0)
296
+ return null;
297
+ const named = signers.map((signer) => `${signer.principals.join(',')} (${signer.keyType}, line ${signer.line})`).join(', ');
298
+ return (`note: signers whose key is not a FIDO2 key: ${named}. ` +
299
+ "Where an agent runs as the person, ADR-0006 recommends a FIDO2 key, ssh-keygen -t ed25519-sk, whose signature needs a touch no process can supply, or a key the agent's account cannot read; " +
300
+ 'a PIV or PKCS#11 hardware key reads as a plain ssh-rsa or ecdsa line, so this is a note, not a failure');
301
+ }
302
+ /** The note doctor gives on a line of the allowed-signers file that is not a signer. */
303
+ export function describeSignerProblem(problem) {
304
+ return `note: line ${problem.line} is not a signer: ${problem.message}`;
305
+ }
306
+ /** What doctor says of the Claude Code on `PATH`, which runs the guard's hooks only from the minimum on. */
307
+ export function describeClaudeRelease(check) {
308
+ switch (check.state) {
309
+ case 'ok':
310
+ return `Claude Code ${check.version} runs the hooks, which need ${CLAUDE_CODE_MINIMUM} or later`;
311
+ case 'outdated':
312
+ return `Claude Code ${check.version} is older than ${CLAUDE_CODE_MINIMUM}, which the hooks need: it ignores a hook's args and runs a bare node, which fails, and a PreToolUse hook that fails blocks nothing, so every write passes unguarded: update Claude Code, with claude update`;
313
+ case 'unknown':
314
+ return `whether Claude Code is ${CLAUDE_CODE_MINIMUM} or later, which the hooks need, cannot be told: ${check.reason}; an older release lets every write pass unguarded, so check claude --version where Claude Code runs`;
315
+ }
316
+ }
317
+ /**
318
+ * The advice for a missing hook, which init gives beside the file and doctor
319
+ * with it. The hook is the guard for a write the agent's hooks never see,
320
+ * one made through a shell (ADR-0005).
321
+ */
322
+ export function missingGitHook(file) {
323
+ const where = file === null ? '' : ` (${file})`;
324
+ return `no pre-commit hook runs spec-harness${where}: one refuses a commit that changes what the active brief protects, for any agent or none, a shell's writes included; run spec-harness init --git-hook --write to add it`;
325
+ }
326
+ /**
327
+ * How to keep the files the rules live in from changing unread, on each
328
+ * forge, as spec-core's ADR-0005 asks: the forge protects them, and CI checks
329
+ * a change against the base branch's rules. GitLab Free has no Code Owners
330
+ * approval, so there the person who merges is the reviewer.
331
+ */
332
+ export const PROTECT_RULE_FILES = "Have the forge protect this file, the ADRs, the tool configurations and the CI configuration, so no change to them reaches the base branch without a person, and check each change in CI against the base branch's rules: " +
333
+ 'on GitHub, CODEOWNERS and a protected branch; on GitLab Premium, Code Owners; on GitLab Free, a protected branch no one pushes to, merged by Maintainers, agents as Developers, and pipelines that must succeed. ' +
334
+ "spec-core's docs/adopting.md has the settings";
335
+ /** The line to add to a hook the repository has of its own. */
336
+ export const GIT_HOOK_LINE = 'npx --no-install spec-harness hook git';
337
+ /** What doctor says of git's pre-commit hook. */
338
+ export function describeGitHook(hook) {
339
+ switch (hook.state) {
340
+ case 'runs':
341
+ return `${hook.file} runs spec-harness`;
342
+ case 'inert':
343
+ return `${hook.file} runs spec-harness, but is not executable, so git skips it: chmod +x ${hook.file}`;
344
+ case 'other':
345
+ return `${hook.file} does not run spec-harness: add the line "${GIT_HOOK_LINE}" to it`;
346
+ case 'absent':
347
+ return missingGitHook(hook.file);
348
+ case 'unknown':
349
+ return `where git runs its hooks cannot be told: ${hook.reason}`;
350
+ }
351
+ }
279
352
  /** spec-graph's configuration files, in the order spec-graph reads them; then a "spec-graph" key in package.json. */
280
353
  export const SPEC_GRAPH_CONFIGS = ['.spec-graph.json', 'spec-graph.config.json'];
281
354
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAIpE,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,SAAS,WAAW,CAAC,KAAc,EAAE,QAA2B;IAC9D,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5H,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,GAAG,GAAG,4DAA4D,CAAC;AAEzE,sGAAsG;AACtG,MAAM,CAAC,MAAM,WAAW,GAAG,uBAAuB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AAE9D,MAAM,YAAY,GAAG,mCAAmC,CAAC;AACzD,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AACrD,MAAM,UAAU,GAAsB,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,IAAI,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjG;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAmB,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;AAE7H,iFAAiF;AACjF,MAAM,WAAW,GAAG,2CAA2C,CAAC;AAChE,MAAM,kBAAkB,GAAG,CAAC,cAAc,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC;AAEnE,SAAS,OAAO,CAAC,IAAa;IAC5B,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;AAC/F,CAAC;AAED,SAAS,aAAa,CAAC,IAAa;IAClC,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC;AAC3D,CAAC;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChF,CAAC;AAED,sHAAsH;AACtH,SAAS,QAAQ,CAAC,KAAc;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,EAAE,GAAI,KAAc,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAI,IAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AAC1J,CAAC;AAED,0HAA0H;AAC1H,MAAM,UAAU,mBAAmB,CAAC,OAAa;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAAS,EAAE,GAAG,KAAK,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAc,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,OAAO;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACxG,IAAI,QAAQ,IAAI,CAAC,OAAO,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;YACrB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtD,CAAC;AAED,8IAA8I;AAC9I,MAAM,UAAU,SAAS,CAAC,UAAkB;IAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,UAAU,IAAI,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,SAAS,cAAc,CAAC,MAAe;IACrC,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,KAAK,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAC5G,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAa;IACpC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACzC,IAAI,cAAc,IAAI,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACxE,OAAO,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,EAAE,GAAI,QAA6B,EAAE,GAAG,SAAS,CAAC,mBAAmB,CAAC,EAAE,EAAE,EAAE,CAAC;AAC9I,CAAC;AAED,oGAAoG;AACpG,SAAS,KAAK,CAAC,IAAa,EAAE,GAAW;IACvC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAChD,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,UAAU,CAAC,QAAiB;IAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9H,CAAC,CAAC,CAAC;AACL,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACzC,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,cAAc,IAAI,OAAO,CAAC;AACxD,CAAC;AAcD,+HAA+H;AAC/H,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,OAAkC;IAC9D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;IACxD,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,OAAO,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACjC,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/C,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,OAAO,EAAE,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/F,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC;AACvE,CAAC;AAED,4DAA4D;AAC5D,SAAS,IAAI,CAAC,MAAqB;IACjC,OAAO,kCAAkC,MAAM,CAAC,EAAE,OAAO,MAAM,CAAC,IAAI,GAAG,CAAC;AAC1E,CAAC;AAED,mFAAmF;AACnF,SAAS,OAAO,CAAC,MAAqB;IACpC,OAAO,mEAAmE,MAAM,CAAC,EAAE,gBAAgB,CAAC;AACtG,CAAC;AAED,MAAM,KAAK,GAAG;IACZ,KAAK,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,8BAA8B,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,EAAE;IACrJ,MAAM,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,gCAAgC,EAAE,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,yBAAyB,EAAE,IAAI,EAAE,IAAI,EAAE;CAC/I,CAAC;AAEX;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB,EAAE,IAAwB,EAAE,OAAgB;IAC/F,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,OAAO;QAAE,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,MAAM,gCAAgC,KAAK,CAAC,KAAK,UAAU,KAAK,CAAC,KAAK,yBAAyB,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACzK,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,OAAO,gBAAgB,KAAK,CAAC,KAAK,aAAa,KAAK,CAAC,IAAI,qDAAqD,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;AACpN,CAAC;AAaD,yHAAyH;AACzH,MAAM,UAAU,WAAW,CAAC,MAAwB;IAClD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC;IACtD,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC1D,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAClC,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,kBAAkB,CAAC,MAAwB;IACzD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAC1B,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChJ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,iJAAiJ,CAAC;QAC7K,OAAO,mBAAmB,OAAO,EAAE,CAAC;IACtC,CAAC;IACD,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,4CAA4C,CAAC;IACvF,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtI,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,OAAO,KAAK,KAAK,kEAAkE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;AACzI,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,MAAM,GAAG,6CAA6C,CAAC;AAEpE,+FAA+F;AAC/F,MAAM,CAAC,MAAM,kBAAkB,GAAsB,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAE7F,gGAAgG;AAChG,MAAM,UAAU,WAAW,CAAC,MAAY;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC;AAChI,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,eAAe,CAAC,MAAY;IAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,OAAa,EAAE,IAAI,GAAkB,IAAI;IACtE,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,KAAK,IAAI,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAC3D,IAAI,MAAM,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,OAAO,GAAc,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvF,MAAM,SAAS,GAAS,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,OAAO;QACL,GAAG,OAAO;QACV,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC;AACJ,CAAC;AAOD,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,QAAQ;YACX,OAAO,2CAA2C,KAAK,CAAC,IAAI,gEAAgE,CAAC;QAC/H,KAAK,YAAY;YACf,OAAO,2HAA2H,MAAM,qBAAqB,KAAK,CAAC,IAAI,oCAAoC,CAAC;QAC9M,KAAK,YAAY;YACf,OAAO,GAAG,KAAK,CAAC,IAAI,uFAAuF,CAAC;QAC9G,KAAK,cAAc;YACjB,OAAO,oIAAoI,CAAC;IAChJ,CAAC;AACH,CAAC;AAkBD;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IACxC,IAAI,aAAa,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,4BAA4B,aAAa,+BAA+B,EAAE,CAAC;IAC7I,MAAM,UAAU,GAAG,+DAA+D,CAAC;IACnF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,YAAY,GAAG,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,QAAQ,CAAC;QAC9D,IAAI,YAAY,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,CAAC;YACpE,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,MAAM,KAAK,KAAK,KAAK,UAAU,8CAA8C,EAAE,CAAC;QAC7I,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,MAAM,yCAAyC,CAAC;IACxG,OAAO;QACL,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,wBAAwB,UAAU,SAAS,KAAK,uGAAuG;KAChK,CAAC;AACJ,CAAC;AAKD,MAAM,OAAO,GAAyC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;AAE9I,kEAAkE;AAClE,MAAM,UAAU,YAAY,CAAC,IAAqH;IAChJ,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;IACpD,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC5F,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAmB,EAAE,MAAe;IAChF,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,GAAG,IAAI,wEAAwE,CAAC;IAC1G,IAAI,MAAM;QAAE,OAAO,GAAG,IAAI,UAAU,IAAI,EAAE,CAAC;IAC3C,OAAO,GAAG,IAAI,cAAc,IAAI,uGAAuG,CAAC;AAC1I,CAAC;AAED,qHAAqH;AACrH,MAAM,CAAC,MAAM,kBAAkB,GAAsB,CAAC,kBAAkB,EAAE,wBAAwB,CAAC,CAAC;AAEpG;;;;;GAKG;AACH,MAAM,mBAAmB,GAAsB,CAAC,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;AAEvI,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,MAAY,EAAE,MAAc;IAC3D,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnJ,MAAM,IAAI,GAAG,aAAa,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;IACvJ,OAAO,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,MAAM,eAAe,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB,EAAE,MAAc,EAAE,KAAc,EAAE,KAAc;IAC/F,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,WAAW,aAAa,CAAC,CAAC,CAAC,GAAG,WAAW,aAAa,CAAC;IACvF,IAAI,KAAK;QAAE,OAAO,GAAG,OAAO,sHAAsH,CAAC;IACnJ,OAAO,GAAG,OAAO,qEAAqE,MAAM,0CAA0C,MAAM,0CAA0C,CAAC;AACzL,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAAC,OAAa,EAAE,WAAmB;IAC/D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAE,OAAO,CAAC,iBAAiB,CAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7J,IAAI,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,EAAE,GAAG,OAAO,EAAE,eAAe,EAAE,CAAC,GAAG,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["/**\n * What `init` writes into each tool's configuration, and what `doctor` says\n * of the result.\n *\n * Pure: `setup.ts` and the command line read the files and ask git, and\n * write what these return, so the core sweep holds these to the unit suite.\n * Each merge keeps what a person wrote and returns `null` when there is\n * nothing to add.\n */\n\nimport { parseGlobList } from './vendor/spec-core/pattern/index.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null;\n}\n\nfunction sameStrings(value: unknown, expected: readonly string[]): boolean {\n return Array.isArray(value) && value.length === expected.length && value.every((item, index) => item === expected[index]);\n}\n\n/**\n * The command line as the project installed it. Claude Code starts a plugin's\n * server in the plugin's own directory and a hook wherever the session\n * stands, so neither can rely on `npx` finding the project's install; and on\n * Windows a server's command is started without a shell, where `npx` is a\n * shim that cannot start at all. `node` and the script's path work\n * everywhere.\n */\nconst BIN = 'node_modules/@descent-vtt/spec-harness/bin/spec-harness.js';\n\n/** The project root, as Claude Code substitutes it in a hook's arguments and in a plugin server's. */\nexport const PROJECT_DIR = '${CLAUDE_PROJECT_DIR}';\n\n/**\n * The project root in a project's own `.mcp.json`. Claude Code expands it\n * there from its own environment, which does not hold it, so it needs a\n * default; `.` is right, since a project's server starts in the project.\n */\nexport const PROJECT_DIR_OR_HERE = '${CLAUDE_PROJECT_DIR:-.}';\n\nconst HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';\nconst GUARDED_EVENTS = ['PreToolUse', 'PostToolUse'];\nconst GUARD_ARGS: readonly string[] = Object.freeze([`${PROJECT_DIR}/${BIN}`, 'hook', 'claude']);\n\n/**\n * The guard as a Claude Code hook: exec form, `args` given, so the project's\n * path is one argument whatever it holds, and no shell is involved.\n */\nexport const GUARD_HOOK: Readonly<Json> = Object.freeze({ type: 'command', command: 'node', args: GUARD_ARGS, timeout: 60 });\n\n/** What 0.1 installed: an npx command, found from wherever the session stood. */\nconst LEGACY_HOOK = 'npx --no-install spec-harness hook claude';\nconst LEGACY_SERVER_ARGS = ['--no-install', 'spec-harness', 'mcp'];\n\nfunction isGuard(hook: unknown): boolean {\n return isObject(hook) && hook['command'] === 'node' && sameStrings(hook['args'], GUARD_ARGS);\n}\n\nfunction isLegacyGuard(hook: unknown): boolean {\n return isObject(hook) && hook['command'] === LEGACY_HOOK;\n}\n\nfunction hooksOf(group: unknown): unknown[] {\n return isObject(group) && Array.isArray(group['hooks']) ? group['hooks'] : [];\n}\n\n/** A group with 0.1's guard replaced where it stands, keeping its other fields; the group itself when it has none. */\nfunction upgraded(group: unknown): unknown {\n const hooks = hooksOf(group);\n if (!hooks.some(isLegacyGuard)) return group;\n return { ...(group as Json), hooks: hooks.map((hook) => (isLegacyGuard(hook) ? { ...(hook as Json), command: 'node', args: [...GUARD_ARGS] } : hook)) };\n}\n\n/** `.claude/settings.json`, or the plugin's hooks, with the guard hooks merged in; `null` when they are already there. */\nexport function mergeClaudeSettings(current: Json): Json | null {\n const hooks = isObject(current['hooks']) ? current['hooks'] : {};\n let changed = false;\n const next: Json = { ...hooks };\n for (const event of GUARDED_EVENTS) {\n const listed = hooks[event];\n const before: unknown[] = Array.isArray(listed) ? listed : [];\n const groups = before.map(upgraded);\n const replaced = groups.some((group, index) => group !== before[index]);\n const present = groups.some((group) => hooksOf(group).some(isGuard));\n if (!present) groups.push({ matcher: HOOK_MATCHER, hooks: [{ ...GUARD_HOOK, args: [...GUARD_ARGS] }] });\n if (replaced || !present) {\n next[event] = groups;\n changed = true;\n }\n }\n return changed ? { ...current, hooks: next } : null;\n}\n\n/** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */\nexport function mcpServer(projectDir: string): Json {\n return { command: 'node', args: [`${projectDir}/${BIN}`, 'mcp', '--root', projectDir] };\n}\n\nfunction isLegacyServer(server: unknown): boolean {\n return isObject(server) && server['command'] === 'npx' && sameStrings(server['args'], LEGACY_SERVER_ARGS);\n}\n\n/**\n * A project's `.mcp.json` with the harness's server merged in, or `null` when\n * one is registered. A server registered under the name some other way is\n * the person's, and stays; 0.1's npx entry is replaced, keeping its other\n * fields.\n */\nexport function mergeMcp(current: Json): Json | null {\n const servers = isObject(current['mcpServers']) ? current['mcpServers'] : {};\n const existing = servers['spec-harness'];\n if ('spec-harness' in servers && !isLegacyServer(existing)) return null;\n return { ...current, mcpServers: { ...servers, 'spec-harness': { ...(existing as Json | undefined), ...mcpServer(PROJECT_DIR_OR_HERE) } } };\n}\n\n/** A key of a file read as JSON; a file that is missing, or is not a JSON object, holds nothing. */\nfunction field(file: unknown, key: string): unknown {\n return isObject(file) ? file[key] : undefined;\n}\n\n/** Whether Claude Code settings hold the guard hooks, as this release or 0.1 wrote them. */\nexport function holdsGuard(settings: unknown): boolean {\n const hooks = field(settings, 'hooks');\n if (!isObject(hooks)) return false;\n return GUARDED_EVENTS.some((event) => {\n const groups = hooks[event];\n return Array.isArray(groups) && groups.some((group) => hooksOf(group).some((hook) => isGuard(hook) || isLegacyGuard(hook)));\n });\n}\n\n/** Whether a project's `.mcp.json` registers a server under the harness's name, however it runs it. */\nexport function registersServer(mcp: unknown): boolean {\n const servers = field(mcp, 'mcpServers');\n return isObject(servers) && 'spec-harness' in servers;\n}\n\n/** A Claude Code settings file, named as a person finds it, and what reading it gave: an object, or nothing usable. */\nexport interface ClaudeSettings {\n readonly file: string;\n readonly settings: unknown;\n}\n\n/** The plugin's id in `enabledPlugins`, and the settings file that turns it on. */\nexport interface EnabledPlugin {\n readonly id: string;\n readonly file: string;\n}\n\n/** `enabledPlugins` keys a plugin `<name>@<marketplace>`; this repository's marketplace lists the plugin as `spec-harness`. */\nconst PLUGIN_ID = /^spec-harness@[^@]+$/;\n\n/**\n * Whether Claude Code runs this package's plugin, from its settings files\n * given lowest precedence first: user, project, local. Claude Code merges\n * `enabledPlugins` key by key, so for each id the last file that sets it to\n * `true` or `false` decides. A plugin named `spec-harness` counts from any\n * marketplace, since another can list this repository. `null` when no file\n * turns it on.\n */\nexport function enabledPlugin(sources: readonly ClaudeSettings[]): EnabledPlugin | null {\n const decided = new Map<string, EnabledPlugin | null>();\n for (const { file, settings } of sources) {\n const plugins = field(settings, 'enabledPlugins');\n if (!isObject(plugins)) continue;\n for (const [id, on] of Object.entries(plugins)) {\n if (PLUGIN_ID.test(id) && typeof on === 'boolean') decided.set(id, on ? { id, file } : null);\n }\n }\n return [...decided.values()].find((entry) => entry !== null) ?? null;\n}\n\n/** Where the plugin is on, as a person would look it up. */\nfunction isOn(plugin: EnabledPlugin): string {\n return `the spec-harness plugin is on (${plugin.id} in ${plugin.file})`;\n}\n\n/** How to have init's entries in place of the plugin's, for this project alone. */\nfunction turnOff(plugin: EnabledPlugin): string {\n return `turn the plugin off for this project with claude plugin disable ${plugin.id} --scope local`;\n}\n\nconst PARTS = {\n hooks: { brings: 'the guard hooks', twice: 'every write is guarded twice', skipped: 'init writes none', entry: \"spec-harness's hooks\", them: 'them' },\n server: { brings: 'the server', twice: 'the server is registered twice', skipped: 'init registers none', entry: 'the spec-harness server', them: 'it' },\n} as const;\n\n/**\n * What init says of a Claude Code file it leaves alone because the plugin is\n * on and brings the same `part`. A file that holds init's entry already is a\n * double install for the person to settle: init removes nothing, since the\n * file is the repository's and a clone without the plugin relies on it.\n */\nexport function describeSkipped(plugin: EnabledPlugin, part: 'hooks' | 'server', present: boolean): string {\n const words = PARTS[part];\n if (present) return `${isOn(plugin)} and brings ${words.brings} this file holds as well, so ${words.twice}: take ${words.entry} out of this file, or ${turnOff(plugin)}`;\n return `${isOn(plugin)} and brings ${words.brings}, so ${words.skipped}: with both, ${words.twice}. To have ${words.them} here instead, for every clone of the repository, ${turnOff(plugin)} and run init again`;\n}\n\n/** How Claude Code is wired to the harness: the plugin, init's entries, both or neither. */\nexport interface ClaudeCodeWiring {\n readonly plugin: EnabledPlugin | null;\n /** The settings files that hold the guard hooks. */\n readonly hooks: readonly string[];\n /** Whether `.mcp.json` registers the server. */\n readonly server: boolean;\n}\n\nexport type WiringState = 'plugin' | 'init' | 'both' | 'none';\n\n/** Which of the four the wiring is. `both` is a double install, and doctor fails it; either of init's entries counts. */\nexport function wiringState(wiring: ClaudeCodeWiring): WiringState {\n const init = wiring.hooks.length > 0 || wiring.server;\n if (wiring.plugin === null) return init ? 'init' : 'none';\n return init ? 'both' : 'plugin';\n}\n\n/** What `doctor` says of the wiring, and what to do when it is doubled or missing. */\nexport function describeClaudeCode(wiring: ClaudeCodeWiring): string {\n const { plugin } = wiring;\n const entries = [...wiring.hooks.map((file) => `the guard hooks in ${file}`), ...(wiring.server ? ['the server in .mcp.json'] : [])].join(', ');\n if (plugin === null) {\n if (entries === '') return 'neither the spec-harness plugin nor the hooks init writes, so Claude Code guards no write: run spec-harness init --write, or install the plugin';\n return `init's entries: ${entries}`;\n }\n if (entries === '') return `${isOn(plugin)} and brings the guard hooks and the server`;\n const twice = [...(wiring.hooks.length > 0 ? [PARTS.hooks.twice] : []), ...(wiring.server ? [PARTS.server.twice] : [])].join(' and ');\n return `${isOn(plugin)}, beside ${entries}: ${twice}. Keep one: take spec-harness's entries out of those files, or ${turnOff(plugin)}`;\n}\n\n/** The spec-brief plugin this package ships, as spec-brief's `plugins` names it. */\nexport const PLUGIN = '@descent-vtt/spec-harness/spec-brief-plugin';\n\n/** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */\nexport const SPEC_BRIEF_CONFIGS: readonly string[] = ['.spec-brief.json', 'spec-brief.json'];\n\n/** Whether spec-brief's configuration loads the plugin, by name or as `{ module, options }`. */\nexport function loadsPlugin(config: Json): boolean {\n const plugins = config['plugins'];\n return Array.isArray(plugins) && plugins.some((entry) => entry === PLUGIN || (isObject(entry) && entry['module'] === PLUGIN));\n}\n\n/** Whether spec-brief's configuration names the base its archive measures a round from. */\nexport function measuresArchive(config: Json): boolean {\n const archiving = config['archiving'];\n return isObject(archiving) && typeof archiving['base'] === 'string';\n}\n\n/**\n * spec-brief's configuration with the plugin loaded and the archive measured\n * from `base`, or `null` when both already hold. spec-brief's archive\n * refuses a round that changed a protected file, and learns that a signed\n * ruling allows it only by asking the plugin (ADR-0006). It checks the file at\n * all only when it knows the base: without one it warns that the scope went\n * unmeasured and passes, so the gate is open until a base is named. A base a\n * person wrote is kept.\n */\nexport function mergeSpecBrief(current: Json, base: string | null = null): Json | null {\n const plugin = loadsPlugin(current);\n const measure = base !== null && !measuresArchive(current);\n if (plugin && !measure) return null;\n const plugins: unknown[] = Array.isArray(current['plugins']) ? current['plugins'] : [];\n const archiving: Json = isObject(current['archiving']) ? current['archiving'] : {};\n return {\n ...current,\n ...(plugin ? {} : { plugins: [...plugins, PLUGIN] }),\n ...(measure ? { archiving: { ...archiving, base } } : {}),\n };\n}\n\n/** Whether spec-brief loads the plugin, as its configuration at the root says. */\nexport type PluginState =\n | { readonly kind: 'loaded' | 'not-loaded' | 'unreadable'; readonly file: string }\n | { readonly kind: 'unconfigured' };\n\n/** What a person reads about the plugin, and what to do when it is not loaded. */\nexport function describePlugin(state: PluginState): string {\n switch (state.kind) {\n case 'loaded':\n return `spec-brief loads spec-harness's plugin (${state.file}): its archive accepts a protected file a signed ruling allows`;\n case 'not-loaded':\n return `spec-brief does not load spec-harness's plugin, so its archive refuses a protected file whatever ruling is signed: add \"${PLUGIN}\" to \"plugins\" in ${state.file}, or run spec-harness init --write`;\n case 'unreadable':\n return `${state.file} cannot be read as JSON, so whether spec-brief loads spec-harness's plugin is unknown`;\n case 'unconfigured':\n return \"spec-brief has no configuration at the root, so it loads no plugin: spec-harness init --write writes one that loads spec-harness's\";\n }\n}\n\n/** What git says that tells a base apart. */\nexport interface BaseFacts {\n /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\n readonly remoteDefault: string | null;\n /** The branch checked out, or `null` on a detached head. */\n readonly branch: string | null;\n /** Every local branch. */\n readonly branches: readonly string[];\n}\n\n/** The base `init` names, and why; `base` is `null` when none can be told. */\nexport interface BaseChoice {\n readonly base: string | null;\n readonly detail: string;\n}\n\n/**\n * The base `init` writes into `.spec-harness.json`. Every ruling is verified\n * against the allowed signers on it, so it is named rather than left to be\n * found. The remote's default branch comes first, but git records it when\n * it clones, and before 2.48 not on a fetch: a repository made with `git\n * init` and pushed to a remote often has none. There the branch init runs on\n * is the base when it is `main` or `master`, or the only branch; otherwise\n * the person names it.\n */\nexport function chooseBase(facts: BaseFacts): BaseChoice {\n const { remoteDefault, branch } = facts;\n if (remoteDefault !== null) return { base: remoteDefault, detail: `rounds are measured from ${remoteDefault}, the remote's default branch` };\n const unrecorded = 'no remote records a default branch (refs/remotes/origin/HEAD)';\n if (branch !== null) {\n const conventional = branch === 'main' || branch === 'master';\n if (conventional || facts.branches.every((name) => name === branch)) {\n const which = conventional ? 'the branch init runs on' : 'the only branch';\n return { base: branch, detail: `rounds are measured from ${branch}, ${which}: ${unrecorded}; change \"base\" if rounds merge into another` };\n }\n }\n const where = branch === null ? 'HEAD is detached' : `${branch} is not main, master or the only branch`;\n return {\n base: null,\n detail: `no base can be told: ${unrecorded}, and ${where}; set \"base\" to the branch rounds merge into, or run git remote set-head origin --auto and init again`,\n };\n}\n\n/** Where a base came from: `--base`, the configuration, or the remote. */\nexport type BaseSource = 'flag' | 'config' | 'remote';\n\nconst SOURCES: Readonly<Record<BaseSource, string>> = { flag: '--base', config: '.spec-harness.json', remote: \"the remote's default branch\" };\n\n/** The base a command resolved, and how; or why there is none. */\nexport function describeBase(base: { readonly ref: string; readonly source: BaseSource; readonly mergeBase: string } | { readonly reason: string }): string {\n if ('reason' in base) return `none: ${base.reason}`;\n return `${base.ref} (${SOURCES[base.source]}), merge base ${base.mergeBase.slice(0, 12)}`;\n}\n\n/** Whether the allowed-signers file is on the base, where every ruling's signature is checked against it. */\nexport function describeSigners(file: string, base: string | null, onBase: boolean): string {\n if (base === null) return `${file}, read from the base, which could not be resolved: no ruling can count`;\n if (onBase) return `${file} is on ${base}`;\n return `${file} is not on ${base}, so no ruling can count: commit it there, one line per person: <email> namespaces=\"git\" <public key>`;\n}\n\n/** spec-graph's configuration files, in the order spec-graph reads them; then a \"spec-graph\" key in package.json. */\nexport const SPEC_GRAPH_CONFIGS: readonly string[] = ['.spec-graph.json', 'spec-graph.config.json'];\n\n/**\n * spec-graph's own patterns when its configuration names none, its\n * DEFAULT_PATTERNS as of 0.9.0. They decide only what init says, never what\n * it writes: `patterns` in the configuration replaces them, so writing the\n * briefs into it would freeze spec-graph's defaults into the file.\n */\nconst SPEC_GRAPH_DEFAULTS: readonly string[] = ['docs/**/*.md', 'doc/**/*.md', 'adr/**/*.md', 'rfcs/**/*.md', 'specs/**/*.md', '*.md'];\n\n/** Whether spec-graph, configured as `config` says, reads the briefs in the directory `briefs`. */\nexport function graphReadsBriefs(config: Json, briefs: string): boolean {\n const configured = Array.isArray(config['patterns']) ? config['patterns'].filter((pattern): pattern is string => typeof pattern === 'string') : [];\n const list = parseGlobList(configured.length > 0 ? configured : SPEC_GRAPH_DEFAULTS, { dialect: 'path', caseSensitive: true, backslash: 'separator' });\n return list.ok && list.list.match(`${briefs}/001_brief.md`);\n}\n\n/**\n * What the history entry does. spec-graph reads a brief whose status says\n * `archived` as a record already; the entry makes one a record whatever word\n * its status uses, or none - and it does anything only where spec-graph\n * reads the briefs, which its defaults do not in `briefs/`.\n */\nexport function describeGraph(archiveGlob: string, briefs: string, reads: boolean, added: boolean): string {\n const history = added ? `read ${archiveGlob} as history` : `${archiveGlob} is history`;\n if (reads) return `${history}: an archived brief is a record whatever its status says, so a live brief that depends on one is not a stale premise`;\n return `${history}, for when spec-graph reads the briefs; its patterns do not reach ${briefs}/, so it checks none of them now: add \"${briefs}/**/*.md\" to its \"patterns\" if it should`;\n}\n\n/** spec-graph's configuration with the archive read as history, or `null` when it already is. */\nexport function mergeSpecGraph(current: Json, archiveGlob: string): Json | null {\n const history = Array.isArray(current['historyPatterns']) ? (current['historyPatterns'] as unknown[]).filter((p): p is string => typeof p === 'string') : [];\n if (history.includes(archiveGlob)) return null;\n return { ...current, historyPatterns: [...history, archiveGlob] };\n}\n"]}
1
+ {"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `init` writes into each tool's configuration, and what `doctor` says\n * of the result.\n *\n * Pure: `setup.ts` and the command line read the files and ask git, and\n * write what these return, so the core sweep holds these to the unit suite.\n * Each merge keeps what a person wrote and returns `null` when there is\n * nothing to add.\n */\n\nimport { isAbsolute } from 'node:path';\n\nimport type { AllowedSigner } from './signers.js';\nimport { parseGlobList } from './vendor/spec-core/pattern/index.js';\nimport { CLAUDE_CODE_MINIMUM, type ClaudeCodeCheck } from './versions.js';\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null;\n}\n\nfunction sameStrings(value: unknown, expected: readonly string[]): boolean {\n return Array.isArray(value) && value.length === expected.length && value.every((item, index) => item === expected[index]);\n}\n\n/**\n * The command line as the project installed it. Claude Code starts a plugin's\n * server in the plugin's own directory and a hook wherever the session\n * stands, so neither can rely on `npx` finding the project's install; and on\n * Windows a server's command is started without a shell, where `npx` is a\n * shim that cannot start at all. `node` and the script's path work\n * everywhere.\n */\nconst BIN = 'node_modules/@descent-vtt/spec-harness/bin/spec-harness.js';\n\n/** The project root, as Claude Code substitutes it in a hook's arguments and in a plugin server's. */\nexport const PROJECT_DIR = '${CLAUDE_PROJECT_DIR}';\n\n/**\n * The project root in a project's own `.mcp.json`. Claude Code expands it\n * there from its own environment, which does not hold it, so it needs a\n * default; `.` is right, since a project's server starts in the project.\n */\nexport const PROJECT_DIR_OR_HERE = '${CLAUDE_PROJECT_DIR:-.}';\n\nconst HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';\nconst GUARDED_EVENTS = ['PreToolUse', 'PostToolUse'];\nconst GUARD_ARGS: readonly string[] = Object.freeze([`${PROJECT_DIR}/${BIN}`, 'hook', 'claude']);\n\n/**\n * The guard as a Claude Code hook: exec form, `args` given, so the project's\n * path is one argument whatever it holds, and no shell is involved.\n */\nexport const GUARD_HOOK: Readonly<Json> = Object.freeze({ type: 'command', command: 'node', args: GUARD_ARGS, timeout: 60 });\n\n/** What 0.1 installed: an npx command, found from wherever the session stood. */\nconst LEGACY_HOOK = 'npx --no-install spec-harness hook claude';\nconst LEGACY_SERVER_ARGS = ['--no-install', 'spec-harness', 'mcp'];\n\nfunction isGuard(hook: unknown): boolean {\n return isObject(hook) && hook['command'] === 'node' && sameStrings(hook['args'], GUARD_ARGS);\n}\n\nfunction isLegacyGuard(hook: unknown): boolean {\n return isObject(hook) && hook['command'] === LEGACY_HOOK;\n}\n\nfunction hooksOf(group: unknown): unknown[] {\n return isObject(group) && Array.isArray(group['hooks']) ? group['hooks'] : [];\n}\n\n/** A group with 0.1's guard replaced where it stands, keeping its other fields; the group itself when it has none. */\nfunction upgraded(group: unknown): unknown {\n const hooks = hooksOf(group);\n if (!hooks.some(isLegacyGuard)) return group;\n return { ...(group as Json), hooks: hooks.map((hook) => (isLegacyGuard(hook) ? { ...(hook as Json), command: 'node', args: [...GUARD_ARGS] } : hook)) };\n}\n\n/** `.claude/settings.json`, or the plugin's hooks, with the guard hooks merged in; `null` when they are already there. */\nexport function mergeClaudeSettings(current: Json): Json | null {\n const hooks = isObject(current['hooks']) ? current['hooks'] : {};\n let changed = false;\n const next: Json = { ...hooks };\n for (const event of GUARDED_EVENTS) {\n const listed = hooks[event];\n const before: unknown[] = Array.isArray(listed) ? listed : [];\n const groups = before.map(upgraded);\n const replaced = groups.some((group, index) => group !== before[index]);\n const present = groups.some((group) => hooksOf(group).some(isGuard));\n if (!present) groups.push({ matcher: HOOK_MATCHER, hooks: [{ ...GUARD_HOOK, args: [...GUARD_ARGS] }] });\n if (replaced || !present) {\n next[event] = groups;\n changed = true;\n }\n }\n return changed ? { ...current, hooks: next } : null;\n}\n\n/** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */\nexport function mcpServer(projectDir: string): Json {\n return { command: 'node', args: [`${projectDir}/${BIN}`, 'mcp', '--root', projectDir] };\n}\n\nfunction isLegacyServer(server: unknown): boolean {\n return isObject(server) && server['command'] === 'npx' && sameStrings(server['args'], LEGACY_SERVER_ARGS);\n}\n\n/**\n * A project's `.mcp.json` with the harness's server merged in, or `null` when\n * one is registered. A server registered under the name some other way is\n * the person's, and stays; 0.1's npx entry is replaced, keeping its other\n * fields.\n */\nexport function mergeMcp(current: Json): Json | null {\n const servers = isObject(current['mcpServers']) ? current['mcpServers'] : {};\n const existing = servers['spec-harness'];\n if ('spec-harness' in servers && !isLegacyServer(existing)) return null;\n return { ...current, mcpServers: { ...servers, 'spec-harness': { ...(existing as Json | undefined), ...mcpServer(PROJECT_DIR_OR_HERE) } } };\n}\n\n/** A key of a file read as JSON; a file that is missing, or is not a JSON object, holds nothing. */\nfunction field(file: unknown, key: string): unknown {\n return isObject(file) ? file[key] : undefined;\n}\n\n/** Whether Claude Code settings hold the guard hooks, as this release or 0.1 wrote them. */\nexport function holdsGuard(settings: unknown): boolean {\n const hooks = field(settings, 'hooks');\n if (!isObject(hooks)) return false;\n return GUARDED_EVENTS.some((event) => {\n const groups = hooks[event];\n return Array.isArray(groups) && groups.some((group) => hooksOf(group).some((hook) => isGuard(hook) || isLegacyGuard(hook)));\n });\n}\n\n/** Whether a project's `.mcp.json` registers a server under the harness's name, however it runs it. */\nexport function registersServer(mcp: unknown): boolean {\n const servers = field(mcp, 'mcpServers');\n return isObject(servers) && 'spec-harness' in servers;\n}\n\n/** A Claude Code settings file, named as a person finds it, and what reading it gave: an object, or nothing usable. */\nexport interface ClaudeSettings {\n readonly file: string;\n readonly settings: unknown;\n}\n\n/** The plugin's id in `enabledPlugins`, and the settings file that turns it on. */\nexport interface EnabledPlugin {\n readonly id: string;\n readonly file: string;\n}\n\n/** `enabledPlugins` keys a plugin `<name>@<marketplace>`; this repository's marketplace lists the plugin as `spec-harness`. */\nconst PLUGIN_ID = /^spec-harness@[^@]+$/;\n\n/**\n * Whether Claude Code runs this package's plugin, from its settings files\n * given lowest precedence first: user, project, local. Claude Code merges\n * `enabledPlugins` key by key, so for each id the last file that sets it to\n * `true` or `false` decides. A plugin named `spec-harness` counts from any\n * marketplace, since another can list this repository. `null` when no file\n * turns it on.\n */\nexport function enabledPlugin(sources: readonly ClaudeSettings[]): EnabledPlugin | null {\n const decided = new Map<string, EnabledPlugin | null>();\n for (const { file, settings } of sources) {\n const plugins = field(settings, 'enabledPlugins');\n if (!isObject(plugins)) continue;\n for (const [id, on] of Object.entries(plugins)) {\n if (PLUGIN_ID.test(id) && typeof on === 'boolean') decided.set(id, on ? { id, file } : null);\n }\n }\n return [...decided.values()].find((entry) => entry !== null) ?? null;\n}\n\n/** Where the plugin is on, as a person would look it up. */\nfunction isOn(plugin: EnabledPlugin): string {\n return `the spec-harness plugin is on (${plugin.id} in ${plugin.file})`;\n}\n\n/** How to have init's entries in place of the plugin's, for this project alone. */\nfunction turnOff(plugin: EnabledPlugin): string {\n return `turn the plugin off for this project with claude plugin disable ${plugin.id} --scope local`;\n}\n\nconst PARTS = {\n hooks: { brings: 'the guard hooks', twice: 'every write is guarded twice', skipped: 'init writes none', entry: \"spec-harness's hooks\", them: 'them' },\n server: { brings: 'the server', twice: 'the server is registered twice', skipped: 'init registers none', entry: 'the spec-harness server', them: 'it' },\n} as const;\n\n/**\n * What init says of a Claude Code file it leaves alone because the plugin is\n * on and brings the same `part`. A file that holds init's entry already is a\n * double install for the person to settle: init removes nothing, since the\n * file is the repository's and a clone without the plugin relies on it.\n */\nexport function describeSkipped(plugin: EnabledPlugin, part: 'hooks' | 'server', present: boolean): string {\n const words = PARTS[part];\n if (present) return `${isOn(plugin)} and brings ${words.brings} this file holds as well, so ${words.twice}: take ${words.entry} out of this file, or ${turnOff(plugin)}`;\n return `${isOn(plugin)} and brings ${words.brings}, so ${words.skipped}: with both, ${words.twice}. To have ${words.them} here instead, for every clone of the repository, ${turnOff(plugin)} and run init again`;\n}\n\n/** How Claude Code is wired to the harness: the plugin, init's entries, both or neither. */\nexport interface ClaudeCodeWiring {\n readonly plugin: EnabledPlugin | null;\n /** The settings files that hold the guard hooks. */\n readonly hooks: readonly string[];\n /** Whether `.mcp.json` registers the server. */\n readonly server: boolean;\n}\n\nexport type WiringState = 'plugin' | 'init' | 'both' | 'none';\n\n/** Which of the four the wiring is. `both` is a double install, and doctor fails it; either of init's entries counts. */\nexport function wiringState(wiring: ClaudeCodeWiring): WiringState {\n const init = wiring.hooks.length > 0 || wiring.server;\n if (wiring.plugin === null) return init ? 'init' : 'none';\n return init ? 'both' : 'plugin';\n}\n\n/** What `doctor` says of the wiring, and what to do when it is doubled or missing. */\nexport function describeClaudeCode(wiring: ClaudeCodeWiring): string {\n const { plugin } = wiring;\n const entries = [...wiring.hooks.map((file) => `the guard hooks in ${file}`), ...(wiring.server ? ['the server in .mcp.json'] : [])].join(', ');\n if (plugin === null) {\n if (entries === '') return 'neither the spec-harness plugin nor the hooks init writes, so Claude Code guards no write: run spec-harness init --write, or install the plugin';\n return `init's entries: ${entries}`;\n }\n if (entries === '') return `${isOn(plugin)} and brings the guard hooks and the server`;\n const twice = [...(wiring.hooks.length > 0 ? [PARTS.hooks.twice] : []), ...(wiring.server ? [PARTS.server.twice] : [])].join(' and ');\n return `${isOn(plugin)}, beside ${entries}: ${twice}. Keep one: take spec-harness's entries out of those files, or ${turnOff(plugin)}`;\n}\n\n/** The spec-brief plugin this package ships, as spec-brief's `plugins` names it. */\nexport const PLUGIN = '@descent-vtt/spec-harness/spec-brief-plugin';\n\n/** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */\nexport const SPEC_BRIEF_CONFIGS: readonly string[] = ['.spec-brief.json', 'spec-brief.json'];\n\n/**\n * Whether spec-brief's configuration loads the plugin, by name or as\n * `{ module, options }`. spec-brief also loads a plugin by a path, one that\n * starts with `.` or is absolute, read from the root; such a path is the\n * plugin when `isPluginFile` says it names the plugin's file, which only the\n * disk can tell.\n */\nexport function loadsPlugin(config: Json, isPluginFile: (path: string) => boolean = () => false): boolean {\n const plugins = config['plugins'];\n const names = (specifier: unknown): boolean =>\n specifier === PLUGIN || (typeof specifier === 'string' && (specifier.startsWith('.') || isAbsolute(specifier)) && isPluginFile(specifier));\n return Array.isArray(plugins) && plugins.some((entry) => names(entry) || (isObject(entry) && names(entry['module'])));\n}\n\n/** Whether spec-brief's configuration names the base its archive measures a round from. */\nexport function measuresArchive(config: Json): boolean {\n const archiving = config['archiving'];\n return isObject(archiving) && typeof archiving['base'] === 'string';\n}\n\n/**\n * spec-brief's configuration with the plugin loaded and the archive measured\n * from `base`, or `null` when both already hold. spec-brief's archive\n * refuses a round that changed a protected file, and learns that a signed\n * ruling allows it only by asking the plugin (ADR-0006). It checks the file at\n * all only when it knows the base: without one it warns that the scope went\n * unmeasured and passes, so the gate is open until a base is named. A base a\n * person wrote is kept.\n */\nexport function mergeSpecBrief(current: Json, base: string | null = null, isPluginFile?: (path: string) => boolean): Json | null {\n const plugin = loadsPlugin(current, isPluginFile);\n const measure = base !== null && !measuresArchive(current);\n if (plugin && !measure) return null;\n const plugins: unknown[] = Array.isArray(current['plugins']) ? current['plugins'] : [];\n const archiving: Json = isObject(current['archiving']) ? current['archiving'] : {};\n return {\n ...current,\n ...(plugin ? {} : { plugins: [...plugins, PLUGIN] }),\n ...(measure ? { archiving: { ...archiving, base } } : {}),\n };\n}\n\n/** Whether spec-brief loads the plugin, as its configuration at the root says. */\nexport type PluginState =\n | { readonly kind: 'loaded' | 'not-loaded' | 'unreadable'; readonly file: string }\n | { readonly kind: 'unconfigured' };\n\n/** What a person reads about the plugin, and what to do when it is not loaded. */\nexport function describePlugin(state: PluginState): string {\n switch (state.kind) {\n case 'loaded':\n return `spec-brief loads spec-harness's plugin (${state.file}): its archive accepts a protected file a signed ruling allows`;\n case 'not-loaded':\n return `spec-brief does not load spec-harness's plugin, so its archive refuses a protected file whatever ruling is signed: add \"${PLUGIN}\" to \"plugins\" in ${state.file}, or run spec-harness init --write`;\n case 'unreadable':\n return `${state.file} cannot be read as JSON, so whether spec-brief loads spec-harness's plugin is unknown`;\n case 'unconfigured':\n return \"spec-brief has no configuration at the root, so it loads no plugin: spec-harness init --write writes one that loads spec-harness's\";\n }\n}\n\n/** What git says that tells a base apart. */\nexport interface BaseFacts {\n /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\n readonly remoteDefault: string | null;\n /** The branch checked out, or `null` on a detached head. */\n readonly branch: string | null;\n /** Every local branch. */\n readonly branches: readonly string[];\n}\n\n/** The base `init` names, and why; `base` is `null` when none can be told. */\nexport interface BaseChoice {\n readonly base: string | null;\n readonly detail: string;\n}\n\n/**\n * The base `init` writes into `.spec-harness.json`. Every ruling is verified\n * against the allowed signers on it, so it is named rather than left to be\n * found. The remote's default branch comes first, but git records it when\n * it clones, and before 2.48 not on a fetch: a repository made with `git\n * init` and pushed to a remote often has none. There the branch init runs on\n * is the base when it is `main` or `master`, or the only branch; otherwise\n * the person names it.\n */\nexport function chooseBase(facts: BaseFacts): BaseChoice {\n const { remoteDefault, branch } = facts;\n if (remoteDefault !== null) return { base: remoteDefault, detail: `rounds are measured from ${remoteDefault}, the remote's default branch` };\n const unrecorded = 'no remote records a default branch (refs/remotes/origin/HEAD)';\n if (branch !== null) {\n const conventional = branch === 'main' || branch === 'master';\n if (conventional || facts.branches.every((name) => name === branch)) {\n const which = conventional ? 'the branch init runs on' : 'the only branch';\n return { base: branch, detail: `rounds are measured from ${branch}, ${which}: ${unrecorded}; change \"base\" if rounds merge into another` };\n }\n }\n const where = branch === null ? 'HEAD is detached' : `${branch} is not main, master or the only branch`;\n return {\n base: null,\n detail: `no base can be told: ${unrecorded}, and ${where}; set \"base\" to the branch rounds merge into, or run git remote set-head origin --auto and init again`,\n };\n}\n\n/** Where a base came from: `--base`, the configuration, or the remote. */\nexport type BaseSource = 'flag' | 'config' | 'remote';\n\nconst SOURCES: Readonly<Record<BaseSource, string>> = { flag: '--base', config: '.spec-harness.json', remote: \"the remote's default branch\" };\n\n/** The base a command resolved, and how; or why there is none. */\nexport function describeBase(base: { readonly ref: string; readonly source: BaseSource; readonly mergeBase: string } | { readonly reason: string }): string {\n if ('reason' in base) return `none: ${base.reason}`;\n return `${base.ref} (${SOURCES[base.source]}), merge base ${base.mergeBase.slice(0, 12)}`;\n}\n\n/** Whether the allowed-signers file is on the base, where every ruling's signature is checked against it. */\nexport function describeSigners(file: string, base: string | null, onBase: boolean): string {\n if (base === null) return `${file}, read from the base, which could not be resolved: no ruling can count`;\n if (onBase) return `${file} is on ${base}`;\n return `${file} is not on ${base}, so no ruling can count: commit it there, one line per person: <email> namespaces=\"git\" <public key>`;\n}\n\n/**\n * The note doctor gives on the signers whose key is not FIDO2, or `null`\n * when every one's is. A note, never a failure: ADR-0006 accepts a key the\n * agent's account cannot read too, and a PIV or PKCS#11 hardware key is\n * written as a plain key.\n */\nexport function describeSignerKeys(signers: readonly Pick<AllowedSigner, 'line' | 'principals' | 'keyType'>[]): string | null {\n if (signers.length === 0) return null;\n const named = signers.map((signer) => `${signer.principals.join(',')} (${signer.keyType}, line ${signer.line})`).join(', ');\n return (\n `note: signers whose key is not a FIDO2 key: ${named}. ` +\n \"Where an agent runs as the person, ADR-0006 recommends a FIDO2 key, ssh-keygen -t ed25519-sk, whose signature needs a touch no process can supply, or a key the agent's account cannot read; \" +\n 'a PIV or PKCS#11 hardware key reads as a plain ssh-rsa or ecdsa line, so this is a note, not a failure'\n );\n}\n\n/** The note doctor gives on a line of the allowed-signers file that is not a signer. */\nexport function describeSignerProblem(problem: { readonly line: number; readonly message: string }): string {\n return `note: line ${problem.line} is not a signer: ${problem.message}`;\n}\n\n/** What doctor says of the Claude Code on `PATH`, which runs the guard's hooks only from the minimum on. */\nexport function describeClaudeRelease(check: ClaudeCodeCheck): string {\n switch (check.state) {\n case 'ok':\n return `Claude Code ${check.version} runs the hooks, which need ${CLAUDE_CODE_MINIMUM} or later`;\n case 'outdated':\n return `Claude Code ${check.version} is older than ${CLAUDE_CODE_MINIMUM}, which the hooks need: it ignores a hook's args and runs a bare node, which fails, and a PreToolUse hook that fails blocks nothing, so every write passes unguarded: update Claude Code, with claude update`;\n case 'unknown':\n return `whether Claude Code is ${CLAUDE_CODE_MINIMUM} or later, which the hooks need, cannot be told: ${check.reason}; an older release lets every write pass unguarded, so check claude --version where Claude Code runs`;\n }\n}\n\n/** Where git's pre-commit hook stands: git's path for it, shown from the root, and what is there. */\nexport type GitHookState =\n | { readonly state: 'runs' | 'absent' | 'other' | 'inert'; readonly file: string }\n | { readonly state: 'unknown'; readonly reason: string };\n\n/**\n * The advice for a missing hook, which init gives beside the file and doctor\n * with it. The hook is the guard for a write the agent's hooks never see,\n * one made through a shell (ADR-0005).\n */\nexport function missingGitHook(file: string | null): string {\n const where = file === null ? '' : ` (${file})`;\n return `no pre-commit hook runs spec-harness${where}: one refuses a commit that changes what the active brief protects, for any agent or none, a shell's writes included; run spec-harness init --git-hook --write to add it`;\n}\n\n/**\n * How to keep the files the rules live in from changing unread, on each\n * forge, as spec-core's ADR-0005 asks: the forge protects them, and CI checks\n * a change against the base branch's rules. GitLab Free has no Code Owners\n * approval, so there the person who merges is the reviewer.\n */\nexport const PROTECT_RULE_FILES =\n \"Have the forge protect this file, the ADRs, the tool configurations and the CI configuration, so no change to them reaches the base branch without a person, and check each change in CI against the base branch's rules: \" +\n 'on GitHub, CODEOWNERS and a protected branch; on GitLab Premium, Code Owners; on GitLab Free, a protected branch no one pushes to, merged by Maintainers, agents as Developers, and pipelines that must succeed. ' +\n \"spec-core's docs/adopting.md has the settings\";\n\n/** The line to add to a hook the repository has of its own. */\nexport const GIT_HOOK_LINE = 'npx --no-install spec-harness hook git';\n\n/** What doctor says of git's pre-commit hook. */\nexport function describeGitHook(hook: GitHookState): string {\n switch (hook.state) {\n case 'runs':\n return `${hook.file} runs spec-harness`;\n case 'inert':\n return `${hook.file} runs spec-harness, but is not executable, so git skips it: chmod +x ${hook.file}`;\n case 'other':\n return `${hook.file} does not run spec-harness: add the line \"${GIT_HOOK_LINE}\" to it`;\n case 'absent':\n return missingGitHook(hook.file);\n case 'unknown':\n return `where git runs its hooks cannot be told: ${hook.reason}`;\n }\n}\n\n/** spec-graph's configuration files, in the order spec-graph reads them; then a \"spec-graph\" key in package.json. */\nexport const SPEC_GRAPH_CONFIGS: readonly string[] = ['.spec-graph.json', 'spec-graph.config.json'];\n\n/**\n * spec-graph's own patterns when its configuration names none, its\n * DEFAULT_PATTERNS as of 0.9.0. They decide only what init says, never what\n * it writes: `patterns` in the configuration replaces them, so writing the\n * briefs into it would freeze spec-graph's defaults into the file.\n */\nconst SPEC_GRAPH_DEFAULTS: readonly string[] = ['docs/**/*.md', 'doc/**/*.md', 'adr/**/*.md', 'rfcs/**/*.md', 'specs/**/*.md', '*.md'];\n\n/** Whether spec-graph, configured as `config` says, reads the briefs in the directory `briefs`. */\nexport function graphReadsBriefs(config: Json, briefs: string): boolean {\n const configured = Array.isArray(config['patterns']) ? config['patterns'].filter((pattern): pattern is string => typeof pattern === 'string') : [];\n const list = parseGlobList(configured.length > 0 ? configured : SPEC_GRAPH_DEFAULTS, { dialect: 'path', caseSensitive: true, backslash: 'separator' });\n return list.ok && list.list.match(`${briefs}/001_brief.md`);\n}\n\n/**\n * What the history entry does. spec-graph reads a brief whose status says\n * `archived` as a record already; the entry makes one a record whatever word\n * its status uses, or none - and it does anything only where spec-graph\n * reads the briefs, which its defaults do not in `briefs/`.\n */\nexport function describeGraph(archiveGlob: string, briefs: string, reads: boolean, added: boolean): string {\n const history = added ? `read ${archiveGlob} as history` : `${archiveGlob} is history`;\n if (reads) return `${history}: an archived brief is a record whatever its status says, so a live brief that depends on one is not a stale premise`;\n return `${history}, for when spec-graph reads the briefs; its patterns do not reach ${briefs}/, so it checks none of them now: add \"${briefs}/**/*.md\" to its \"patterns\" if it should`;\n}\n\n/** spec-graph's configuration with the archive read as history, or `null` when it already is. */\nexport function mergeSpecGraph(current: Json, archiveGlob: string): Json | null {\n const history = Array.isArray(current['historyPatterns']) ? (current['historyPatterns'] as unknown[]).filter((p): p is string => typeof p === 'string') : [];\n if (history.includes(archiveGlob)) return null;\n return { ...current, historyPatterns: [...history, archiveGlob] };\n}\n"]}
package/dist/context.d.ts CHANGED
@@ -11,7 +11,8 @@
11
11
  * front matter is never closed, whose status cannot be read, rather than
12
12
  * shown as one without a status, and a pattern in the scope, a protection or
13
13
  * a ruling's path the guard cannot read, with spec-core's reason and what the
14
- * guard does without it, rather than listed as one it can.
14
+ * guard does without it, rather than listed as one it can; and one a leading
15
+ * `/` roots, which names no path the guard decides.
15
16
  */
16
17
  import type { BriefRow } from './types.js';
17
18
  export interface CitedDocument {
@@ -96,10 +97,14 @@ export interface ContextPacket {
96
97
  readonly unreadableRulingPaths: readonly UnreadableRulingPath[];
97
98
  }
98
99
  /**
99
- * A brief's title without the id it repeats: `012 - Rotate tokens` read as
100
- * `Rotate tokens`, and the same with an en or an em dash, which `spec-brief
101
- * new` writes, or with a colon after the id. Only the brief's own id is taken
102
- * off: `Fix - the login bug` is a title, not an id and a title.
100
+ * A brief's title without the id it repeats, by the rules spec-brief reads
101
+ * it with: the brief's own id, then an em dash, which `spec-brief new`
102
+ * writes, an en dash, a full-width colon or a colon, spaced or not, or a
103
+ * hyphen with a space on each side; `012 - Rotate tokens`, `012\u2014Rotate
104
+ * tokens` and `012\uFF1ARotate tokens` read as `Rotate tokens`. A bare hyphen
105
+ * is part of a word or a number, as in `001-2 migration`, and only the
106
+ * brief's own id is taken off: `Fix - the login bug` is a title, and so is
107
+ * `0010 \u2014 x` on brief 001.
103
108
  */
104
109
  export declare function titleOf(brief: Pick<BriefRow, 'id' | 'title'>): string | null;
105
110
  /** Renders the packet, filling the budget with cited documents in the order the brief cites them. */
package/dist/context.js CHANGED
@@ -11,10 +11,11 @@
11
11
  * front matter is never closed, whose status cannot be read, rather than
12
12
  * shown as one without a status, and a pattern in the scope, a protection or
13
13
  * a ruling's path the guard cannot read, with spec-core's reason and what the
14
- * guard does without it, rather than listed as one it can.
14
+ * guard does without it, rather than listed as one it can; and one a leading
15
+ * `/` roots, which names no path the guard decides.
15
16
  */
16
17
  import { sameId } from './branch.js';
17
- import { whyUnreadable } from './guard.js';
18
+ import { rooted, whyUnreadable } from './guard.js';
18
19
  /**
19
20
  * spec-guard's answer to `query --json`, read. It exits 2 both when no spec
20
21
  * file matched its patterns - a repository that keeps none, or keeps them
@@ -51,19 +52,29 @@ function list(items) {
51
52
  * Each pattern a line, as the guard reads it: as written, or with spec-core's
52
53
  * reason the guard cannot read it and what the guard does without it. The
53
54
  * guard's own compile decides, so the packet and the guard cannot disagree.
55
+ * With `rootedNote`, a pattern a leading `/` roots, or one of whose
56
+ * alternatives it roots, is marked too, and counted when every alternative
57
+ * is rooted.
54
58
  */
55
- function patternLines(patterns, consequence) {
59
+ function patternLines(patterns, consequence, rootedNote) {
56
60
  const unreadable = [];
61
+ let rootedWhole = 0;
57
62
  const lines = patterns.map((pattern) => {
58
63
  const reason = whyUnreadable(pattern);
59
- if (reason === null)
60
- return `- \`${pattern}\``;
64
+ if (reason === null) {
65
+ const root = rootedNote === undefined ? null : rooted(pattern);
66
+ if (root === null)
67
+ return `- \`${pattern}\``;
68
+ if (root === 'whole')
69
+ rootedWhole += 1;
70
+ return `- \`${pattern}\`, ${rootedNote[root]}`;
71
+ }
61
72
  unreadable.push({ pattern, reason });
62
73
  return `- \`${pattern}\`, which the guard cannot read: ${reason}; ${consequence}`;
63
74
  });
64
- return { lines, unreadable };
75
+ return { lines, unreadable, rootedWhole };
65
76
  }
66
- function rulesSection(rules, scopeUnread) {
77
+ function rulesSection(rules, scopeUnread, scopeRooted) {
67
78
  if ('none' in rules)
68
79
  return `spec-guard holds no rule over this scope: ${rules.none}.`;
69
80
  if ('unavailable' in rules) {
@@ -73,8 +84,11 @@ function rulesSection(rules, scopeUnread) {
73
84
  // With no pattern of the scope readable, spec-guard was asked about no
74
85
  // path, so an empty answer is no answer: "no rule" would tell the agent
75
86
  // the code it writes is unconstrained.
76
- return scopeUnread
77
- ? 'The scope could not be read: no pattern in `affectedFiles` can be read, so spec-guard was not asked for the rules over it. Treat every ADR as binding until the scope is fixed.'
87
+ if (scopeUnread) {
88
+ return 'The scope could not be read: no pattern in `affectedFiles` can be read, so spec-guard was not asked for the rules over it. Treat every ADR as binding until the scope is fixed.';
89
+ }
90
+ return scopeRooted
91
+ ? "No pattern in `affectedFiles` puts a path in the scope: each is rooted at the filesystem's root or cannot be read, so spec-guard was not asked for the rules over it. Treat every ADR as binding until the scope is fixed."
78
92
  : 'spec-guard holds no rule over this scope.';
79
93
  }
80
94
  const byDocument = new Map();
@@ -90,15 +104,19 @@ function rulesSection(rules, scopeUnread) {
90
104
  return parts.join('\n').trimEnd();
91
105
  }
92
106
  /**
93
- * A brief's title without the id it repeats: `012 - Rotate tokens` read as
94
- * `Rotate tokens`, and the same with an en or an em dash, which `spec-brief
95
- * new` writes, or with a colon after the id. Only the brief's own id is taken
96
- * off: `Fix - the login bug` is a title, not an id and a title.
107
+ * A brief's title without the id it repeats, by the rules spec-brief reads
108
+ * it with: the brief's own id, then an em dash, which `spec-brief new`
109
+ * writes, an en dash, a full-width colon or a colon, spaced or not, or a
110
+ * hyphen with a space on each side; `012 - Rotate tokens`, `012\u2014Rotate
111
+ * tokens` and `012\uFF1ARotate tokens` read as `Rotate tokens`. A bare hyphen
112
+ * is part of a word or a number, as in `001-2 migration`, and only the
113
+ * brief's own id is taken off: `Fix - the login bug` is a title, and so is
114
+ * `0010 \u2014 x` on brief 001.
97
115
  */
98
116
  export function titleOf(brief) {
99
117
  if (brief.title === null)
100
118
  return null;
101
- return brief.title.replace(/^\s*([^\s:]+)(?:\s+[-\u2013\u2014]|:)\s+/, (whole, first) => (sameId(first, brief.id) ? '' : whole));
119
+ return brief.title.replace(/^\s*([^\s:\uFF1A\u2013\u2014]+)(?:\s*[:\uFF1A\u2013\u2014]\s*|\s+-\s+)(?=\S)/, (whole, first) => sameId(first, brief.id) ? '' : whole);
102
120
  }
103
121
  /** Renders the packet, filling the budget with cited documents in the order the brief cites them. */
104
122
  export function renderContext(input) {
@@ -128,20 +146,36 @@ export function renderContext(input) {
128
146
  .map((ruling) => {
129
147
  const paths = ruling.paths.map((pattern) => {
130
148
  const reason = whyUnreadable(pattern);
131
- if (reason === null)
149
+ if (reason === null) {
150
+ // A rooted path can be read, and names no path the guard decides.
151
+ const root = rooted(pattern);
152
+ if (root === 'whole')
153
+ return `\`${pattern}\` (which a leading \`/\` roots at the filesystem's root; it allows nothing)`;
154
+ if (root === 'part')
155
+ return `\`${pattern}\` (an alternative of which a leading \`/\` roots at the filesystem's root; that alternative allows nothing)`;
132
156
  return `\`${pattern}\``;
157
+ }
133
158
  unreadableRulingPaths.push({ ruling: ruling.id, pattern, reason });
134
159
  return `\`${pattern}\` (which the guard cannot read: ${reason}; it allows nothing)`;
135
160
  });
136
161
  return `- ${ruling.id}, signed by ${ruling.signer}: ${paths.join(', ')}`;
137
162
  })
138
163
  .join('\n');
139
- const mayWrite = patternLines(brief.affectedFiles, 'it puts no path in the scope');
164
+ // A rooted pattern can be read and puts no path in the scope: every path
165
+ // the guard decides is repository-relative. spec-guard is not asked about it.
166
+ const mayWrite = patternLines(brief.affectedFiles, 'it puts no path in the scope', {
167
+ whole: "which a leading `/` roots at the filesystem's root: it puts no path in the scope, and spec-guard is not asked about it",
168
+ part: "an alternative of which a leading `/` roots at the filesystem's root: that alternative puts no path in the scope, and spec-guard is not asked about it",
169
+ });
140
170
  const unreadableScope = mayWrite.unreadable;
141
171
  // While a protection the guard cannot read stands, the guard refuses every
142
172
  // write, and no ruling waives it; a write to the brief is let through,
143
- // since that is where it is fixed.
144
- const mustNot = patternLines(brief.protectedFiles, 'until it is fixed, the guard refuses every write but to the brief');
173
+ // since that is where it is fixed. A rooted protection can be read, and
174
+ // protects nothing: the file it was meant to protect is the round's to write.
175
+ const mustNot = patternLines(brief.protectedFiles, 'until it is fixed, the guard refuses every write but to the brief', {
176
+ whole: "which a leading `/` roots at the filesystem's root: it protects no path",
177
+ part: "an alternative of which a leading `/` roots at the filesystem's root: that alternative protects no path",
178
+ });
145
179
  const unreadableProtections = mustNot.unreadable;
146
180
  const scope = [
147
181
  '## Scope, as the guard reads it',
@@ -163,7 +197,8 @@ export function renderContext(input) {
163
197
  });
164
198
  const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\n'), ''].join('\n');
165
199
  const scopeUnread = mayWrite.lines.length > 0 && unreadableScope.length === mayWrite.lines.length;
166
- const rules = ['## Rules in force for this scope', '', rulesSection(input.rules, scopeUnread), ''].join('\n');
200
+ const scopeRooted = mayWrite.rootedWhole > 0 && unreadableScope.length + mayWrite.rootedWhole === mayWrite.lines.length;
201
+ const rules = ['## Rules in force for this scope', '', rulesSection(input.rules, scopeUnread, scopeRooted), ''].join('\n');
167
202
  const howTo = [
168
203
  '## How this round works',
169
204
  '',