@descent-vtt/spec-harness 0.17.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +263 -0
- package/README.md +63 -5
- package/dist/cli.d.ts +15 -1
- package/dist/cli.js +334 -85
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +12 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +158 -18
- package/dist/configure.js +318 -55
- package/dist/configure.js.map +1 -1
- package/dist/fs.d.ts +83 -3
- package/dist/fs.js +190 -13
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +66 -7
- package/dist/git.js +80 -8
- package/dist/git.js.map +1 -1
- package/dist/hooks.d.ts +66 -0
- package/dist/hooks.js +112 -15
- package/dist/hooks.js.map +1 -1
- package/dist/round.d.ts +25 -4
- package/dist/round.js +100 -33
- package/dist/round.js.map +1 -1
- package/dist/setup.d.ts +12 -2
- package/dist/setup.js +92 -26
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +18 -4
- package/dist/siblings.js +30 -17
- package/dist/siblings.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +71 -8
- package/dist/usage.js +108 -19
- package/dist/usage.js.map +1 -1
- package/dist/workspace.d.ts +38 -1
- package/dist/workspace.js +90 -15
- package/dist/workspace.js.map +1 -1
- package/hooks/hooks.json +16 -0
- package/package.json +2 -2
package/dist/configure.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* nothing to add.
|
|
9
9
|
*/
|
|
10
10
|
import type { AllowedSigner } from './signers.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type { LocalBranch } from './types.js';
|
|
12
|
+
import { type HookLine } from './usage.js';
|
|
12
13
|
import { type ClaudeCodeCheck } from './versions.js';
|
|
13
14
|
type Json = Record<string, unknown>;
|
|
14
15
|
/** The project root, as Claude Code substitutes it in a hook's arguments and in a plugin server's. */
|
|
@@ -24,6 +25,44 @@ export declare const PROJECT_DIR_OR_HERE = "${CLAUDE_PROJECT_DIR:-.}";
|
|
|
24
25
|
* path is one argument whatever it holds, and no shell is involved.
|
|
25
26
|
*/
|
|
26
27
|
export declare const GUARD_HOOK: Readonly<Json>;
|
|
28
|
+
/**
|
|
29
|
+
* The release of Claude Code from which a comma in a matcher separates
|
|
30
|
+
* names as a `|` does. Its page on debugging a configuration (Check hooks,
|
|
31
|
+
* read 2026-10-14) has a comma before it fall through to the reading of a
|
|
32
|
+
* pattern, under which the matcher never matched, and its changelog has the
|
|
33
|
+
* fix under this release: hooks with comma-separated matchers no longer
|
|
34
|
+
* silently never firing.
|
|
35
|
+
*/
|
|
36
|
+
export declare const COMMA_SINCE = "2.1.191";
|
|
37
|
+
/**
|
|
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.
|
|
44
|
+
*/
|
|
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';
|
|
54
|
+
} | {
|
|
55
|
+
readonly as: 'older';
|
|
56
|
+
readonly release: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly as: 'untold';
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
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.
|
|
64
|
+
*/
|
|
65
|
+
export declare function matchingUnder(release: ClaudeCodeCheck | null): Matching;
|
|
27
66
|
/** What reads a matcher that is a pattern: the writing tools it matches, or `null` where that cannot be told. */
|
|
28
67
|
export type PatternReader = (source: string) => readonly string[] | null;
|
|
29
68
|
/**
|
|
@@ -57,8 +96,34 @@ export declare function patternReader(read?: PatternReader): PatternReader;
|
|
|
57
96
|
*
|
|
58
97
|
* `pattern` reads a matcher that is a pattern: the run's own reader, where
|
|
59
98
|
* the run has read the file's matchers already ({@link patternReader}).
|
|
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
|
|
101
|
+
* release starts for no write is no guard, and init's own is written beside
|
|
102
|
+
* it.
|
|
60
103
|
*/
|
|
61
|
-
export declare function mergeClaudeSettings(current: Json, isHook?: (words: readonly string[]) => boolean, pattern?: PatternReader): 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";
|
|
62
127
|
/** The server's entry, reading the project at `projectDir`: {@link PROJECT_DIR} for the plugin, {@link PROJECT_DIR_OR_HERE} for a project. */
|
|
63
128
|
export declare function mcpServer(projectDir: string): Json;
|
|
64
129
|
/**
|
|
@@ -113,7 +178,8 @@ export interface HookReport {
|
|
|
113
178
|
* `guards`: Claude Code waits on the hook before a write, and the hook
|
|
114
179
|
* checks it. `off`: the hook checks no write, for a line it cannot read, a
|
|
115
180
|
* root that is no directory, or a root that is a directory outside any
|
|
116
|
-
* work tree
|
|
181
|
+
* work tree; or it is never started, in a file an array for a matcher
|
|
182
|
+
* keeps Claude Code from loading the hooks of. `held`: the hook holds every write, for a
|
|
117
183
|
* brief the line names that is not one. `unstarted`: Claude Code waits on
|
|
118
184
|
* the line before no write, so it is not the guard. `unknown`: what the
|
|
119
185
|
* hook is handed, which writes it is started for, or whether Claude Code
|
|
@@ -132,6 +198,30 @@ export interface HookReport {
|
|
|
132
198
|
/** What `onFailure` changes, where the entry sets it to `"block"`. */
|
|
133
199
|
readonly note: string | null;
|
|
134
200
|
}
|
|
201
|
+
/** An array that stands for a matcher where it unloads a settings file's hooks: the event it stands under, and the array as written. */
|
|
202
|
+
export interface ArrayMatcher {
|
|
203
|
+
readonly event: string;
|
|
204
|
+
readonly matcher: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The first array that stands for a matcher under `PreToolUse` or
|
|
208
|
+
* `PermissionRequest` in a settings file, or `null`.
|
|
209
|
+
*
|
|
210
|
+
* Claude Code's page on debugging a configuration
|
|
211
|
+
* (https://code.claude.com/docs/en/debug-your-config, Check hooks, read
|
|
212
|
+
* 2026-10-14) has a matcher that is an array, where it must be one string,
|
|
213
|
+
* as an entry Claude Code lists as an invalid setting, at the start of an
|
|
214
|
+
* interactive session and in `claude doctor`; and where the array is under
|
|
215
|
+
* one of these two events, none of that file's other hooks load either. It
|
|
216
|
+
* names no release. So a file that holds one runs no hook at all, whoever
|
|
217
|
+
* wrote the entry and whatever it runs: the array's own is not loaded, and
|
|
218
|
+
* neither is any other.
|
|
219
|
+
*/
|
|
220
|
+
export declare function arrayMatcher(settings: unknown): ArrayMatcher | null;
|
|
221
|
+
/** What is said of a settings file that an array for a matcher keeps Claude Code from loading the hooks of. */
|
|
222
|
+
export declare function describeArrayMatcher(file: string, { event, matcher }: ArrayMatcher): string;
|
|
223
|
+
/** How a matcher that is an array is mended. */
|
|
224
|
+
export declare const ONE_STRING = "write the matcher as one string, its names between \"|\"";
|
|
135
225
|
/** The words handed over for every line of the harness's in the settings files, for a caller that has to ask something of each before it reports them. */
|
|
136
226
|
export declare function handedLines(sources: readonly ClaudeSettings[], project: string): string[][];
|
|
137
227
|
/**
|
|
@@ -144,16 +234,25 @@ export declare function handedLines(sources: readonly ClaudeSettings[], project:
|
|
|
144
234
|
* A line that guards and holds no more than a release wrote is reported
|
|
145
235
|
* with nothing to say, as it always was. One with more on it is said as it
|
|
146
236
|
* is, and fails nothing. One the hook cannot read, or whose root is no
|
|
147
|
-
* directory, is
|
|
237
|
+
* directory, or is in no work tree that holds the project, is a guard that
|
|
238
|
+
* is off, and one that names a brief which is not
|
|
148
239
|
* one holds every write: each a problem, and doctor exits 1 for it. A line
|
|
149
240
|
* Claude Code waits on before no write is not the guard, whatever the hook
|
|
150
241
|
* would do with it.
|
|
151
242
|
*
|
|
152
243
|
* `pattern` reads a matcher that is a pattern, once for every entry under
|
|
153
244
|
* it in every file: the run's own reader, where something else in the run
|
|
154
|
-
* reads the same files ({@link patternReader}).
|
|
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.
|
|
249
|
+
*
|
|
250
|
+
* A file that holds an array for a matcher under `PreToolUse` or
|
|
251
|
+
* `PermissionRequest` has none of its hooks loaded ({@link arrayMatcher}):
|
|
252
|
+
* each line of the hook there is started for no write, and one that stood
|
|
253
|
+
* to be started for a write is a guard that is off.
|
|
155
254
|
*/
|
|
156
|
-
export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader): HookReport[];
|
|
255
|
+
export declare function reportHooks(sources: readonly ClaudeSettings[], project: string, read: (words: readonly string[]) => HookLine | null, pattern?: PatternReader, matching?: Matching): HookReport[];
|
|
157
256
|
/** Whether a project's `.mcp.json` registers a server under the harness's name, however it runs it. */
|
|
158
257
|
export declare function registersServer(mcp: unknown): boolean;
|
|
159
258
|
/** A Claude Code settings file, named as a person finds it, and what reading it gave: an object, or nothing usable. */
|
|
@@ -278,11 +377,26 @@ export interface ClaudeCodeWiring {
|
|
|
278
377
|
readonly lines: readonly HookReport[];
|
|
279
378
|
/** Whether `.mcp.json` registers the server. */
|
|
280
379
|
readonly server: boolean;
|
|
380
|
+
/** Whether a settings file has Claude Code start the hook as a session starts (startsSessions). */
|
|
381
|
+
readonly sessionStart: boolean;
|
|
382
|
+
/**
|
|
383
|
+
* The release of the Claude Code on `PATH`, where it was asked: a matcher
|
|
384
|
+
* that lists names with a comma is read as that release reads it
|
|
385
|
+
* ({@link matchingUnder}). `null` where it was not asked.
|
|
386
|
+
*/
|
|
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;
|
|
281
394
|
}
|
|
282
395
|
export type WiringState = 'plugin' | 'init' | 'both' | 'none';
|
|
283
|
-
/** 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. */
|
|
284
397
|
type Wired = Pick<ClaudeCodeWiring, 'plugin' | 'hooks' | 'server'> & {
|
|
285
398
|
readonly presence?: PluginPresence | null;
|
|
399
|
+
readonly unloaded?: ArrayMatcher | null;
|
|
286
400
|
};
|
|
287
401
|
/**
|
|
288
402
|
* Which of the four the wiring is. `both` is a double install, and doctor
|
|
@@ -315,8 +429,7 @@ export declare function describePresence(wiring: Wired): string | null;
|
|
|
315
429
|
export declare function describeClaudeCode(wiring: Wired): string;
|
|
316
430
|
/** The spec-brief plugin this package ships, as spec-brief's `plugins` names it. */
|
|
317
431
|
export declare const PLUGIN = "@descent-vtt/spec-harness/spec-brief-plugin";
|
|
318
|
-
|
|
319
|
-
export declare const SPEC_BRIEF_CONFIGS: readonly string[];
|
|
432
|
+
export { SPEC_BRIEF_CONFIGS } from './config.js';
|
|
320
433
|
/**
|
|
321
434
|
* The file a package's `exports` target names for an import, as Node's
|
|
322
435
|
* PACKAGE_TARGET_RESOLVE finds it, or `null` when it names none: a string
|
|
@@ -362,10 +475,10 @@ export declare function describePlugin(state: PluginState): string;
|
|
|
362
475
|
export interface BaseFacts {
|
|
363
476
|
/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */
|
|
364
477
|
readonly remoteDefault: string | null;
|
|
365
|
-
/** The branch checked out, or `null` on a detached head. */
|
|
478
|
+
/** The name of the branch checked out, or `null` on a detached head. */
|
|
366
479
|
readonly branch: string | null;
|
|
367
|
-
/** Every local branch. */
|
|
368
|
-
readonly branches: readonly
|
|
480
|
+
/** Every local branch, by its name and the ref a base names it by. */
|
|
481
|
+
readonly branches: readonly LocalBranch[];
|
|
369
482
|
}
|
|
370
483
|
/** The base `init` names, and why; `base` is `null` when none can be told. */
|
|
371
484
|
export interface BaseChoice {
|
|
@@ -412,6 +525,30 @@ export declare function describeClaudeRelease(check: ClaudeCodeCheck): string;
|
|
|
412
525
|
export type GitHookState = {
|
|
413
526
|
readonly state: 'runs' | 'absent' | 'other' | 'inert';
|
|
414
527
|
readonly file: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* What stands at the path is no file, `what` it is instead. `stops`: git
|
|
531
|
+
* can execute it, tries to run it as the hook, fails, and stops every
|
|
532
|
+
* commit. `ignored`: git cannot execute it, and skips it as it skips a
|
|
533
|
+
* hook that is not executable.
|
|
534
|
+
*/
|
|
535
|
+
| {
|
|
536
|
+
readonly state: 'stops' | 'ignored';
|
|
537
|
+
readonly file: string;
|
|
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;
|
|
415
552
|
} | {
|
|
416
553
|
readonly state: 'unknown';
|
|
417
554
|
readonly reason: string;
|
|
@@ -438,13 +575,17 @@ export declare const PROTECT_RULE_FILES: string;
|
|
|
438
575
|
export declare const GIT_HOOK_LINE = "node node_modules/@descent-vtt/spec-harness/bin/spec-harness.js hook git";
|
|
439
576
|
/**
|
|
440
577
|
* The hook init writes. It is a POSIX sh script on every platform, since Git
|
|
441
|
-
* for Windows runs a hook with the sh it ships.
|
|
442
|
-
*
|
|
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
|
|
443
584
|
* repository that shares its hooks with one that uses the harness - node
|
|
444
|
-
* would end on a stack trace, so the hook says what is missing and where,
|
|
445
|
-
* 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.
|
|
446
587
|
*/
|
|
447
|
-
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
|
|
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";
|
|
448
589
|
/**
|
|
449
590
|
* What doctor and init add of a hook whose text runs the harness through
|
|
450
591
|
* npx: the hook init wrote through 0.9.1, or the line it advised for a hook
|
|
@@ -469,4 +610,3 @@ export declare function graphReadsBriefs(config: Json, briefs: string): boolean;
|
|
|
469
610
|
export declare function describeGraph(archiveGlob: string, briefs: string, reads: boolean, added: boolean): string;
|
|
470
611
|
/** spec-graph's configuration with the archive read as history, or `null` when it already is. */
|
|
471
612
|
export declare function mergeSpecGraph(current: Json, archiveGlob: string): Json | null;
|
|
472
|
-
export {};
|