@descent-vtt/spec-harness 0.18.0 → 0.19.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.
@@ -8,6 +8,7 @@
8
8
  * nothing to add.
9
9
  */
10
10
  import type { AllowedSigner } from './signers.js';
11
+ import type { LocalBranch } from './types.js';
11
12
  import { type HookLine } from './usage.js';
12
13
  import { type ClaudeCodeCheck } from './versions.js';
13
14
  type Json = Record<string, unknown>;
@@ -34,25 +35,34 @@ export declare const GUARD_HOOK: Readonly<Json>;
34
35
  */
35
36
  export declare const COMMA_SINCE = "2.1.191";
36
37
  /**
37
- * How the Claude Code that runs the hooks reads a comma in a matcher of
38
- * names: as a separator, from {@link COMMA_SINCE} on; as part of a pattern,
39
- * in the release named, which is older; or it cannot be told, where the
40
- * release cannot.
38
+ * The release of Claude Code from which a hyphenated name in a matcher of
39
+ * names is matched exactly. Its changelog
40
+ * (https://code.claude.com/docs/en/changelog, 2.1.195, read 2026-10-18) has,
41
+ * among its fixes, hook matchers with hyphenated identifiers that matched as
42
+ * substrings, and match exactly from this release on; it does not say how
43
+ * the rest of such a matcher was read.
41
44
  */
42
- export type Commas = {
43
- readonly as: 'separator';
45
+ export declare const HYPHEN_SINCE = "2.1.195";
46
+ /**
47
+ * How the Claude Code that runs the hooks reads a matcher of names: as every
48
+ * release from {@link HYPHEN_SINCE} on, and the hooks reference today; as
49
+ * the release named, which is older; or it cannot be told, where the release
50
+ * cannot.
51
+ */
52
+ export type Matching = {
53
+ readonly as: 'today';
44
54
  } | {
45
- readonly as: 'pattern';
55
+ readonly as: 'older';
46
56
  readonly release: string;
47
57
  } | {
48
58
  readonly as: 'untold';
49
59
  };
50
60
  /**
51
- * How the Claude Code doctor asked its release reads a comma. A release that
52
- * was not asked, or could not be told, leaves the reading untold: asked or
53
- * not, nothing says which it is.
61
+ * How the Claude Code doctor asked its release reads a matcher of names. A
62
+ * release that was not asked, or could not be told, leaves the reading
63
+ * untold: asked or not, nothing says which it is.
54
64
  */
55
- export declare function commasUnder(release: ClaudeCodeCheck | null): Commas;
65
+ export declare function matchingUnder(release: ClaudeCodeCheck | null): Matching;
56
66
  /** What reads a matcher that is a pattern: the writing tools it matches, or `null` where that cannot be told. */
57
67
  export type PatternReader = (source: string) => readonly string[] | null;
58
68
  /**
@@ -86,12 +96,34 @@ export declare function patternReader(read?: PatternReader): PatternReader;
86
96
  *
87
97
  * `pattern` reads a matcher that is a pattern: the run's own reader, where
88
98
  * the run has read the file's matchers already ({@link patternReader}).
89
- * `commas` is how the Claude Code here reads a comma in a matcher of names,
90
- * where the run asked its release ({@link commasUnder}): a line that
99
+ * `matching` is how the Claude Code here reads a matcher of names, where
100
+ * the run asked its release ({@link matchingUnder}): a line that
91
101
  * release starts for no write is no guard, and init's own is written beside
92
102
  * it.
93
103
  */
94
- export declare function mergeClaudeSettings(current: Json, isHook?: (words: readonly string[]) => boolean, pattern?: PatternReader, commas?: Commas): Json | null;
104
+ export declare function mergeClaudeSettings(current: Json, isHook?: (words: readonly string[]) => boolean, pattern?: PatternReader, matching?: Matching): Json | null;
105
+ /**
106
+ * Settings with init's entry under `SessionStart` added beside its two, or
107
+ * `null` where it is not to be: where init's own line does not stand
108
+ * under both `PreToolUse` and `PostToolUse`, a file init writes no hook
109
+ * into or one it has yet to complete; and where a line of the hook stands
110
+ * under `SessionStart` already, init's or one `isHook` knows, which would
111
+ * say the same twice (ADR-0012).
112
+ */
113
+ export declare function withSessionStart(settings: Json, isHook: (words: readonly string[]) => boolean): Json | null;
114
+ /**
115
+ * Whether settings have Claude Code start the hook as a session starts: an
116
+ * entry under `SessionStart` whose line is init's, or one `isHook` knows
117
+ * as the hook's. Its matcher is not read: one that names some of the ways a
118
+ * session starts has the check made for those.
119
+ */
120
+ export declare function startsSessions(settings: unknown, isHook: (words: readonly string[]) => boolean): boolean;
121
+ /**
122
+ * What doctor notes where settings hold the guard hooks and none under
123
+ * `SessionStart`: what would hold every write is said at the first write,
124
+ * which it holds, and not as the session starts.
125
+ */
126
+ export declare const NO_SESSION_START = "note: no settings file has Claude Code start the hook as a session starts, so a configuration that does not load, or a brief named that spec-brief does not list, is said only at the first write, which it holds: spec-harness init --write adds the entry beside the two it wrote";
95
127
  /** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */
96
128
  export declare function mcpServer(projectDir: string): Json;
97
129
  /**
@@ -210,17 +242,17 @@ export declare function handedLines(sources: readonly ClaudeSettings[], project:
210
242
  *
211
243
  * `pattern` reads a matcher that is a pattern, once for every entry under
212
244
  * it in every file: the run's own reader, where something else in the run
213
- * reads the same files ({@link patternReader}). `commas` is how the Claude
214
- * Code here reads a comma in a matcher of names, where its release was
215
- * asked ({@link commasUnder}); a caller that asks none reads a comma as
216
- * every release from {@link COMMA_SINCE} on does.
245
+ * reads the same files ({@link patternReader}). `matching` is how the
246
+ * Claude Code here reads a matcher of names, where its release was asked
247
+ * ({@link matchingUnder}); a caller that asks none reads one as every
248
+ * release from {@link HYPHEN_SINCE} on does.
217
249
  *
218
250
  * A file that holds an array for a matcher under `PreToolUse` or
219
251
  * `PermissionRequest` has none of its hooks loaded ({@link arrayMatcher}):
220
252
  * each line of the hook there is started for no write, and one that stood
221
253
  * to be started for a write is a guard that is off.
222
254
  */
223
- export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader, commas?: Commas): HookReport[];
255
+ export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader, matching?: Matching): HookReport[];
224
256
  /** Whether a project's `.mcp.json` registers a server under the harness's name, however it runs it. */
225
257
  export declare function registersServer(mcp: unknown): boolean;
226
258
  /** A Claude Code settings file, named as a person finds it, and what reading it gave: an object, or nothing usable. */
@@ -345,17 +377,26 @@ export interface ClaudeCodeWiring {
345
377
  readonly lines: readonly HookReport[];
346
378
  /** Whether `.mcp.json` registers the server. */
347
379
  readonly server: boolean;
380
+ /** Whether a settings file has Claude Code start the hook as a session starts (startsSessions). */
381
+ readonly sessionStart: boolean;
348
382
  /**
349
383
  * The release of the Claude Code on `PATH`, where it was asked: a matcher
350
384
  * that lists names with a comma is read as that release reads it
351
- * ({@link commasUnder}). `null` where it was not asked.
385
+ * ({@link matchingUnder}). `null` where it was not asked.
352
386
  */
353
387
  readonly release: ClaudeCodeCheck | null;
388
+ /**
389
+ * An array for a matcher that keeps Claude Code from loading the hooks of
390
+ * the project's settings file, the file init writes its hooks into, or
391
+ * `null`: init writes none there while it stands ({@link arrayMatcher}).
392
+ */
393
+ readonly unloaded: ArrayMatcher | null;
354
394
  }
355
395
  export type WiringState = 'plugin' | 'init' | 'both' | 'none';
356
- /** The wiring as its descriptions read it: what a caller knows of the plugin's presence, which one that does not ask leaves out. */
396
+ /** The wiring as its descriptions read it: what a caller knows of the plugin's presence, and of an array in the project's settings, which one that does not ask leaves out. */
357
397
  type Wired = Pick<ClaudeCodeWiring, 'plugin' | 'hooks' | 'server'> & {
358
398
  readonly presence?: PluginPresence | null;
399
+ readonly unloaded?: ArrayMatcher | null;
359
400
  };
360
401
  /**
361
402
  * Which of the four the wiring is. `both` is a double install, and doctor
@@ -388,8 +429,7 @@ export declare function describePresence(wiring: Wired): string | null;
388
429
  export declare function describeClaudeCode(wiring: Wired): string;
389
430
  /** The spec-brief plugin this package ships, as spec-brief's `plugins` names it. */
390
431
  export declare const PLUGIN = "@descent-vtt/spec-harness/spec-brief-plugin";
391
- /** spec-brief's configuration files, in the order spec-brief looks for them in a directory. */
392
- export declare const SPEC_BRIEF_CONFIGS: readonly string[];
432
+ export { SPEC_BRIEF_CONFIGS } from './config.js';
393
433
  /**
394
434
  * The file a package's `exports` target names for an import, as Node's
395
435
  * PACKAGE_TARGET_RESOLVE finds it, or `null` when it names none: a string
@@ -435,10 +475,10 @@ export declare function describePlugin(state: PluginState): string;
435
475
  export interface BaseFacts {
436
476
  /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */
437
477
  readonly remoteDefault: string | null;
438
- /** The branch checked out, or `null` on a detached head. */
478
+ /** The name of the branch checked out, or `null` on a detached head. */
439
479
  readonly branch: string | null;
440
- /** Every local branch. */
441
- readonly branches: readonly string[];
480
+ /** Every local branch, by its name and the ref a base names it by. */
481
+ readonly branches: readonly LocalBranch[];
442
482
  }
443
483
  /** The base `init` names, and why; `base` is `null` when none can be told. */
444
484
  export interface BaseChoice {
@@ -496,6 +536,19 @@ export type GitHookState = {
496
536
  readonly state: 'stops' | 'ignored';
497
537
  readonly file: string;
498
538
  readonly what: string;
539
+ }
540
+ /**
541
+ * A file the account may not read, `reason` being why, and `runs` whether
542
+ * git may execute it. One git may not execute it skips, with a hint, and
543
+ * commits. One it may it runs, and a script the shell cannot read either
544
+ * stops every commit there (measured with git 2.47 as an ordinary account),
545
+ * where a program runs without being read: which it is cannot be read.
546
+ */
547
+ | {
548
+ readonly state: 'unreadable';
549
+ readonly file: string;
550
+ readonly runs: boolean;
551
+ readonly reason: string;
499
552
  } | {
500
553
  readonly state: 'unknown';
501
554
  readonly reason: string;
@@ -522,13 +575,17 @@ export declare const PROTECT_RULE_FILES: string;
522
575
  export declare const GIT_HOOK_LINE = "node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git";
523
576
  /**
524
577
  * The hook init writes. It is a POSIX sh script on every platform, since Git
525
- * for Windows runs a hook with the sh it ships. Where the project's install
526
- * is not in the work tree - a linked worktree nobody installed into, a
578
+ * for Windows runs a hook with the sh it ships. git starts it at the top of
579
+ * the work tree it commits in, so the project's install is read from there,
580
+ * and where a linked worktree has none, from the repository's main work
581
+ * tree: the work tree whose `.git` the common directory is, where `git
582
+ * worktree add` was run and the project installed (ADR-0005). Where neither
583
+ * has the install - a worktree of a repository nobody installed into, a
527
584
  * repository that shares its hooks with one that uses the harness - node
528
- * would end on a stack trace, so the hook says what is missing and where, and
529
- * the commit waits, as it did when npx found nothing to run.
585
+ * would end on a stack trace, so the hook says what is missing and where,
586
+ * and the commit waits, as it did when npx found nothing to run.
530
587
  */
531
- export declare const PRE_COMMIT = "#!/bin/sh\n# spec-harness: refuse a commit that changes what the active brief protects.\n# git runs this at the top of the work tree it commits in, a linked worktree's\n# own, wherever the hook is kept: the project's install is read from there.\nbin=node_modules/@descent-vtt/spec-harness/bin/spec-harness.js\nif [ ! -f \"$bin\" ]; then\n printf '%s\\n' \"spec-harness: $bin is not in $PWD, the work tree git commits in: install the project's dependencies there, as with npm ci, and commit again\" >&2\n exit 2\nfi\nexec node \"$bin\" hook git\n";
588
+ export declare const PRE_COMMIT = "#!/bin/sh\n# spec-harness: refuse a commit that changes what the active brief protects.\n# git runs this at the top of the work tree it commits in, a linked worktree's\n# own, wherever the hook is kept: the project's install is read from there,\n# or, where a linked worktree has none, from the repository's main work tree.\nbin=node_modules/@descent-vtt/spec-harness/bin/spec-harness.js\nif [ ! -f \"$bin\" ]; then\n common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)\n if [ \"${common##*/}\" = .git ] && [ -f \"${common%/.git}/$bin\" ]; then\n bin=${common%/.git}/$bin\n else\n printf '%s\\n' \"spec-harness: $bin is not in $PWD, the work tree git commits in, nor in its repository's main work tree: install the project's dependencies there, as with npm ci, and commit again\" >&2\n exit 2\n fi\nfi\nexec node \"$bin\" hook git\n";
532
589
  /**
533
590
  * What doctor and init add of a hook whose text runs the harness through
534
591
  * npx: the hook init wrote through 0.9.1, or the line it advised for a hook
@@ -553,4 +610,3 @@ export declare function graphReadsBriefs(config: Json, briefs: string): boolean;
553
610
  export declare function describeGraph(archiveGlob: string, briefs: string, reads: boolean, added: boolean): string;
554
611
  /** spec-graph's configuration with the archive read as history, or `null` when it already is. */
555
612
  export declare function mergeSpecGraph(current: Json, archiveGlob: string): Json | null;
556
- export {};