@descent-vtt/spec-harness 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +372 -0
- package/README.md +78 -11
- package/dist/briefs.d.ts +45 -0
- package/dist/briefs.js +73 -0
- package/dist/briefs.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +384 -84
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +7 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +15 -1
- package/dist/config.js +16 -3
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +142 -32
- package/dist/configure.js +316 -89
- package/dist/configure.js.map +1 -1
- package/dist/fs.d.ts +150 -3
- package/dist/fs.js +459 -17
- package/dist/fs.js.map +1 -1
- package/dist/git.d.ts +85 -9
- package/dist/git.js +140 -16
- package/dist/git.js.map +1 -1
- package/dist/hooks.d.ts +111 -0
- package/dist/hooks.js +172 -16
- package/dist/hooks.js.map +1 -1
- package/dist/round.d.ts +8 -0
- package/dist/round.js +32 -9
- package/dist/round.js.map +1 -1
- package/dist/sandbox.js +4 -3
- package/dist/sandbox.js.map +1 -1
- package/dist/server.d.ts +25 -1
- package/dist/server.js +194 -27
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +6 -3
- package/dist/setup.js +74 -23
- package/dist/setup.js.map +1 -1
- package/dist/siblings.d.ts +42 -7
- package/dist/siblings.js +135 -30
- 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 +66 -12
- package/dist/usage.js +134 -21
- package/dist/usage.js.map +1 -1
- package/dist/workspace.d.ts +80 -1
- package/dist/workspace.js +188 -8
- package/dist/workspace.js.map +1 -1
- package/hooks/hooks.json +16 -0
- package/package.json +2 -2
package/dist/workspace.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* runs in, and which brief the round is working on.
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
7
|
import { parseArgs } from 'node:util';
|
|
7
8
|
import { findActive } from './briefs.js';
|
|
8
9
|
import { briefIdFromBranch, ciBranch } from './branch.js';
|
|
9
|
-
import { holdsGit, isTop, loadConfig } from './fs.js';
|
|
10
|
+
import { holdsGit, isTop, loadConfig, mainWorkTree, realSpelling, repositoryPath } from './fs.js';
|
|
10
11
|
import { commonDirectory, currentBranch, workTree, workTreeRoot } from './git.js';
|
|
11
12
|
import { createSiblings } from './siblings.js';
|
|
12
13
|
export const EXIT_OK = 0;
|
|
@@ -48,6 +49,7 @@ export function parseOptions(argv) {
|
|
|
48
49
|
strict: { type: 'boolean' },
|
|
49
50
|
write: { type: 'boolean' },
|
|
50
51
|
'git-hook': { type: 'boolean' },
|
|
52
|
+
'block-on-failure': { type: 'boolean' },
|
|
51
53
|
path: { type: 'string', multiple: true },
|
|
52
54
|
reason: { type: 'string' },
|
|
53
55
|
option: { type: 'string', multiple: true },
|
|
@@ -64,7 +66,7 @@ export function parseOptions(argv) {
|
|
|
64
66
|
};
|
|
65
67
|
let parsed;
|
|
66
68
|
try {
|
|
67
|
-
parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options });
|
|
69
|
+
parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options, tokens: true });
|
|
68
70
|
}
|
|
69
71
|
catch (error) {
|
|
70
72
|
// Read again with nothing refused, for whose line it was and nothing
|
|
@@ -73,7 +75,25 @@ export function parseOptions(argv) {
|
|
|
73
75
|
// so that a value is not taken for the command it stands before.
|
|
74
76
|
throw new UnreadableLine(error.message, parseArgs({ args: [...argv], strict: false, options }).positionals);
|
|
75
77
|
}
|
|
76
|
-
const { values, positionals } = parsed;
|
|
78
|
+
const { values, positionals, tokens } = parsed;
|
|
79
|
+
// The parser keeps the last value of an option given twice, and the earlier
|
|
80
|
+
// went unread with no word of it: `--root nowhere --root .` ran in `.`. So
|
|
81
|
+
// every value is kept, for the refusal of two (usage.ts). A list keeps
|
|
82
|
+
// every value already.
|
|
83
|
+
const table = options;
|
|
84
|
+
const written = {};
|
|
85
|
+
for (const token of tokens) {
|
|
86
|
+
if (token.kind === 'option' && token.value !== undefined && table[token.name].multiple !== true)
|
|
87
|
+
(written[token.name] ??= []).push(token.value);
|
|
88
|
+
}
|
|
89
|
+
// Two answers asked for at once: the version was printed, and the help went
|
|
90
|
+
// unread. A line that is wrong, whoever's it is (spec-core ADR-0005). Only
|
|
91
|
+
// an option's token has a name, so the test of its kind is the compiler's,
|
|
92
|
+
// and its mutant is equivalent.
|
|
93
|
+
const answers = tokens.flatMap((token) => (token.kind === 'option' && (token.name === 'help' || token.name === 'version') ? [token] : []));
|
|
94
|
+
const other = answers.find((token) => token.name !== answers[0].name);
|
|
95
|
+
if (other !== undefined)
|
|
96
|
+
throw new UnreadableLine(`${answers[0].rawName} is given beside ${other.rawName}, and one of the two answers would go unread; give one of them`, positionals);
|
|
77
97
|
const asked = values.format ?? 'pretty';
|
|
78
98
|
const format = asked;
|
|
79
99
|
const formats = ['pretty', 'json', 'gitlab', 'sarif', 'github'];
|
|
@@ -91,6 +111,7 @@ export function parseOptions(argv) {
|
|
|
91
111
|
command,
|
|
92
112
|
positionals: positionals.slice(1),
|
|
93
113
|
given: Object.keys(values),
|
|
114
|
+
written,
|
|
94
115
|
brief: values.brief,
|
|
95
116
|
base: values.base,
|
|
96
117
|
root: values.root,
|
|
@@ -100,6 +121,7 @@ export function parseOptions(argv) {
|
|
|
100
121
|
version: values.version === true,
|
|
101
122
|
write: values.write === true,
|
|
102
123
|
gitHook: values['git-hook'] === true,
|
|
124
|
+
blockOnFailure: values['block-on-failure'] === true,
|
|
103
125
|
paths: values.path ?? [],
|
|
104
126
|
reason: values.reason,
|
|
105
127
|
options: values.option ?? [],
|
|
@@ -113,8 +135,8 @@ export function parseOptions(argv) {
|
|
|
113
135
|
at,
|
|
114
136
|
};
|
|
115
137
|
}
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
/** The work tree that holds `cwd`, or `null` where none does. */
|
|
139
|
+
export async function findTree(cwd) {
|
|
118
140
|
// Each git command is a process, a few milliseconds on Linux and a few
|
|
119
141
|
// hundred on a Windows workstation, and every command opens a work tree,
|
|
120
142
|
// the hook before every write among them. So the top and the common
|
|
@@ -124,9 +146,44 @@ export async function openWorkspace(options, io) {
|
|
|
124
146
|
// it did: below a top it would be asked twice, and in no work tree for
|
|
125
147
|
// nothing.
|
|
126
148
|
const [tree, early] = await Promise.all([workTree(cwd), holdsGit(cwd) ? currentBranch(cwd) : undefined]);
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
if (tree === null)
|
|
150
|
+
return null;
|
|
151
|
+
const root = tree === 'unread' ? await workTreeRoot(cwd) : tree.root;
|
|
152
|
+
return root === null ? null : { cwd, root, tree, early };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The git directory a found work tree's repository keeps, as git names it
|
|
156
|
+
* from the directory it was found from: one repository's work trees, linked
|
|
157
|
+
* and main, share it, and no two repositories do. Asked again only where
|
|
158
|
+
* the one start of git could not be read.
|
|
159
|
+
*/
|
|
160
|
+
export async function foundCommonDir(found) {
|
|
161
|
+
// Asked again from the top, git names the directory the one start named:
|
|
162
|
+
// the mutant that always asks again is equivalent, at a process's cost.
|
|
163
|
+
return found.tree === 'unread' ? commonDirectory(found.root) : found.tree.commonDir;
|
|
164
|
+
}
|
|
165
|
+
export async function openWorkspace(options, io, installs = MAIN_INSTALL) {
|
|
166
|
+
const cwd = options.root ?? io.cwd;
|
|
167
|
+
const found = await findTree(cwd);
|
|
168
|
+
if (found === null)
|
|
129
169
|
throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);
|
|
170
|
+
return openFound(found, io, installs);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Every command: a linked worktree with no install of its own is checked
|
|
174
|
+
* with the main work tree's, where `git worktree add` leaves the project's
|
|
175
|
+
* `node_modules`, as git's hook and doctor were, and as the hook is with the
|
|
176
|
+
* project's (ADR-0005).
|
|
177
|
+
*/
|
|
178
|
+
export const MAIN_INSTALL = (root, commonDir) => {
|
|
179
|
+
const main = mainWorkTree(commonDir);
|
|
180
|
+
// The main work tree's own install is the one looked in first: the mutant
|
|
181
|
+
// that keeps it here as well looks in it twice and finds what it found.
|
|
182
|
+
return main === null || main === root ? [] : [main];
|
|
183
|
+
};
|
|
184
|
+
/** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */
|
|
185
|
+
export async function openFound(found, io, installs = MAIN_INSTALL) {
|
|
186
|
+
const { cwd, root, tree, early } = found;
|
|
130
187
|
// The common directory and the branch are asked of the root. What git
|
|
131
188
|
// said from `cwd` is that answer where `cwd` is the root, the same
|
|
132
189
|
// directory asked the same thing, and nowhere else. Measured with git
|
|
@@ -146,7 +203,7 @@ export async function openWorkspace(options, io) {
|
|
|
146
203
|
// out, asks the forge which branch the run builds.
|
|
147
204
|
const ci = checkedOut === null ? ciBranch(io.env) : null;
|
|
148
205
|
const named = ci === null ? { branch: checkedOut } : { branch: ci.branch, branchSource: ci.source };
|
|
149
|
-
return { root, commonDir, config, siblings: createSiblings(root, config), ...named, cwd: io.cwd };
|
|
206
|
+
return { root, commonDir, config, siblings: createSiblings(root, config, installs(root, commonDir)), ...named, cwd: io.cwd };
|
|
150
207
|
}
|
|
151
208
|
/**
|
|
152
209
|
* What one of several things asked at once answered, or what it threw,
|
|
@@ -218,4 +275,127 @@ export function describeActive(active, argument = 'the argument') {
|
|
|
218
275
|
export function json(command, body) {
|
|
219
276
|
return `${JSON.stringify({ tool: 'spec-harness', version: version(), schemaVersion: JSON_SCHEMA_VERSION, command, ...body }, null, 2)}\n`;
|
|
220
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether a work tree other than the one whose top is `top` may hold `cwd`:
|
|
280
|
+
* `cwd` is outside it, or a `.git` stands in a directory between the two,
|
|
281
|
+
* where a linked worktree, a submodule or a repository of its own starts.
|
|
282
|
+
* Where neither is so, `cwd` is in that work tree, and git is asked nothing
|
|
283
|
+
* more: a session works at its project's top, or below it, for nearly every
|
|
284
|
+
* write. Answering that one may is what git is then asked, so the mutant
|
|
285
|
+
* that always answers so is equivalent, at the cost of a process, and so
|
|
286
|
+
* is each that only answers so more often: a slice one short reaches the
|
|
287
|
+
* top itself, which holds a `.git`, and a part named in place of none is
|
|
288
|
+
* a directory no top holds.
|
|
289
|
+
*/
|
|
290
|
+
export function mayBeElsewhere(cwd, top) {
|
|
291
|
+
const path = repositoryPath(cwd, top, cwd);
|
|
292
|
+
if (path === null)
|
|
293
|
+
return true;
|
|
294
|
+
const parts = path === '' ? [] : path.split('/');
|
|
295
|
+
return parts.some((_, index) => holdsGit(join(top, ...parts.slice(0, index + 1))));
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The work tree that holds `cwd`, where it is another work tree of the
|
|
299
|
+
* repository the work tree `named` is one of, as the hook's root names it, by the git directory the two share;
|
|
300
|
+
* `null` where `cwd` is in the root's own, in another repository, or in
|
|
301
|
+
* none (ADR-0005).
|
|
302
|
+
*/
|
|
303
|
+
export async function otherTree(named, cwd) {
|
|
304
|
+
// Asked anyway, git names the root's own top for a directory in its work
|
|
305
|
+
// tree, and none for one not made, either of which answers `null` below:
|
|
306
|
+
// the mutant that always asks is equivalent, at a process's cost.
|
|
307
|
+
if (!mayBeElsewhere(cwd, named.root))
|
|
308
|
+
return null;
|
|
309
|
+
const found = await findTree(cwd);
|
|
310
|
+
// The project's own work tree, found again, would decide every path the
|
|
311
|
+
// same way once more: the check saves reading its configuration twice, and
|
|
312
|
+
// its mutant is equivalent.
|
|
313
|
+
if (found === null || found.root === named.root)
|
|
314
|
+
return null;
|
|
315
|
+
const common = await foundCommonDir(found);
|
|
316
|
+
// git names a common directory wherever it names a top, so `null` is
|
|
317
|
+
// there for the type.
|
|
318
|
+
if (common === null || realSpelling(common) !== realSpelling(named.commonDir))
|
|
319
|
+
return null;
|
|
320
|
+
return found;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The top of the nearest work tree, of any repository, above a directory or
|
|
324
|
+
* a file: the nearest directory above it that holds a `.git`, found with no
|
|
325
|
+
* process; `null` where none does. A path not yet written has the
|
|
326
|
+
* directories it would be written in looked through, those not made yet
|
|
327
|
+
* among them.
|
|
328
|
+
*/
|
|
329
|
+
function topAbove(path) {
|
|
330
|
+
for (let directory = dirname(path);; directory = dirname(directory)) {
|
|
331
|
+
if (holdsGit(directory))
|
|
332
|
+
return directory;
|
|
333
|
+
// Only the top of the filesystem is its own parent.
|
|
334
|
+
if (dirname(directory) === directory)
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The innermost work tree of the repository the work tree `named` is one of
|
|
340
|
+
* that holds a path, where it is not `named` itself: a linked worktree or the
|
|
341
|
+
* main one, inside the project, beside it, or around it (ADR-0005). The
|
|
342
|
+
* directories above the path are looked through, with no process, for each
|
|
343
|
+
* that holds a `.git`; git is asked of each that is not `named`'s top, and
|
|
344
|
+
* one that git names as no work tree of this repository's, a submodule's, a
|
|
345
|
+
* clone's inside the tree, or a `.git` that stands in no work tree's top, is
|
|
346
|
+
* passed by for the next above it. `null` where the first of this repository
|
|
347
|
+
* met is `named`, or none is: the path is decided by `named`, as before, a
|
|
348
|
+
* path in no work tree of the repository among them.
|
|
349
|
+
*/
|
|
350
|
+
async function treeHolding(named, path, from) {
|
|
351
|
+
const own = realSpelling(named.root);
|
|
352
|
+
// An absolute path, as Claude Code sends one, is what resolve leaves it.
|
|
353
|
+
for (let top = topAbove(realSpelling(resolve(from, path))); top !== null; top = topAbove(top)) {
|
|
354
|
+
if (top === own)
|
|
355
|
+
return null;
|
|
356
|
+
const tree = await otherTree(named, top);
|
|
357
|
+
// git names a work tree whose top is not this directory where it reads
|
|
358
|
+
// the `.git` here as no repository's, an empty directory say, and names
|
|
359
|
+
// the one around it: the walk meets that one's top next, past any `.git`
|
|
360
|
+
// between that git reads as none too, and takes it. So the mutant that
|
|
361
|
+
// takes what git names here is equivalent for every work tree git makes;
|
|
362
|
+
// the check keeps a `core.worktree` set by hand to name another
|
|
363
|
+
// directory from deciding a path that directory does not hold.
|
|
364
|
+
if (tree !== null && realSpelling(tree.root) === top)
|
|
365
|
+
return tree;
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The work trees a write is decided in, each with the paths it decides, for
|
|
371
|
+
* the hook and for the server's `check_path` (ADR-0005). The repository is
|
|
372
|
+
* the one the hook's root names, as before. Each path is decided by the
|
|
373
|
+
* innermost work tree of that repository that holds it, by its branch, its
|
|
374
|
+
* briefs and its configuration, read from its top, with its siblings looked
|
|
375
|
+
* for in its install and then the project's: there a session works in a
|
|
376
|
+
* worktree while Claude Code names the project it started in, or writes from
|
|
377
|
+
* one into another, and the hook decided by the project's branch, which
|
|
378
|
+
* named no brief, so a protected write passed with no word. A path in no
|
|
379
|
+
* other work tree of the repository is decided by the work tree the root
|
|
380
|
+
* names, as before: a write to the project's own file from a worktree is
|
|
381
|
+
* refused where it was, and a path in another repository, or in none, is
|
|
382
|
+
* read as it was. `cwd` is what a relative path is read from.
|
|
383
|
+
*/
|
|
384
|
+
export async function writesBy(named, cwd, paths, io) {
|
|
385
|
+
const from = cwd ?? io.cwd;
|
|
386
|
+
const trees = [];
|
|
387
|
+
// The parts in the order their work trees are first met, each with its
|
|
388
|
+
// paths in the order they were named.
|
|
389
|
+
for (const path of paths) {
|
|
390
|
+
const tree = await treeHolding(named, path, from);
|
|
391
|
+
const part = trees.find((other) => other.tree?.root === tree?.root);
|
|
392
|
+
if (part === undefined)
|
|
393
|
+
trees.push({ tree, paths: [path] });
|
|
394
|
+
else
|
|
395
|
+
part.paths.push(path);
|
|
396
|
+
}
|
|
397
|
+
// The root's own last, as it was decided after the work tree Claude works in.
|
|
398
|
+
const ordered = [...trees.filter((part) => part.tree !== null), ...trees.filter((part) => part.tree === null)];
|
|
399
|
+
return Promise.all(ordered.map(async ({ tree, paths: held }) => ({ workspace: tree === null ? named : await openFound(tree, io, () => [named.root]), paths: held })));
|
|
400
|
+
}
|
|
221
401
|
//# sourceMappingURL=workspace.js.map
|
package/dist/workspace.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What every command shares: exit codes, options, the repository a command\n * runs in, and which brief the round is working on.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { parseArgs, type ParseArgsOptionsConfig } from 'node:util';\n\nimport { findActive, type ActiveBrief, type ActiveSource } from './briefs.js';\nimport { briefIdFromBranch, ciBranch } from './branch.js';\nimport type { HarnessConfig } from './config.js';\nimport { holdsGit, isTop, loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch, workTree, workTreeRoot } from './git.js';\nimport type { FindingFormat } from './formats.js';\nimport { createSiblings, type Siblings } from './siblings.js';\nimport type { BriefRow } from './types.js';\n\nexport const EXIT_OK = 0;\nexport const EXIT_FAILED = 1;\nexport const EXIT_ERROR = 2;\n\n/** The version of the JSON documents this tool prints. Bumped when a field changes meaning. */\nexport const JSON_SCHEMA_VERSION = 1;\n\nexport class UsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'UsageError';\n }\n}\n\n/**\n * A command line the parser refused, with the command and the arguments on\n * it. Whose line it was decides what the refusal is: Claude Code's hook says\n * it and holds no write for it, where every other command stops (cli.ts).\n */\nexport class UnreadableLine extends UsageError {\n readonly words: readonly string[];\n\n constructor(message: string, words: readonly string[]) {\n super(message);\n this.words = words;\n }\n}\n\nexport interface CliIO {\n readonly stdout: { write(text: string): unknown };\n readonly stderr: { write(text: string): unknown };\n readonly stdin?: () => Promise<string>;\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n}\n\nexport function version(): string {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n const manifest: { version: string } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));\n return manifest.version;\n}\n\nexport interface Options {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option given, in the order given: what tells an option left out from one a command does not read. */\n readonly given: readonly string[];\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n /** `gitlab`, `sarif` and `github` only for `audit` and `premises`, whose findings have places. */\n readonly format: 'pretty' | 'json' | FindingFormat;\n readonly strict: boolean;\n readonly help: boolean;\n readonly version: boolean;\n /** init: apply the plan instead of printing it. */\n readonly write: boolean;\n /** init: also install git's pre-commit hook. */\n readonly gitHook: boolean;\n /** escalate: the files the round needs, the reason, the options and the recommendation. */\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly list: boolean;\n readonly show: string | undefined;\n /** rule: the decision and what it allows. */\n readonly allow: boolean;\n readonly deny: boolean;\n readonly note: string | undefined;\n /** probe: which probe, and at which commit. */\n readonly id: string | undefined;\n readonly at: 'base' | 'head' | 'both' | undefined;\n}\n\nexport function parseOptions(argv: readonly string[]): Options {\n // `satisfies`, where a const assertion would read the same to the compiler:\n // the sweep mutates nothing that stands under an `as`, the table with it.\n const options = {\n brief: { type: 'string' },\n base: { type: 'string' },\n root: { type: 'string' },\n format: { type: 'string' },\n strict: { type: 'boolean' },\n write: { type: 'boolean' },\n 'git-hook': { type: 'boolean' },\n path: { type: 'string', multiple: true },\n reason: { type: 'string' },\n option: { type: 'string', multiple: true },\n recommend: { type: 'string' },\n list: { type: 'boolean' },\n show: { type: 'string' },\n allow: { type: 'boolean' },\n deny: { type: 'boolean' },\n note: { type: 'string' },\n id: { type: 'string' },\n at: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n } satisfies ParseArgsOptionsConfig;\n let parsed;\n try {\n parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options });\n } catch (error) {\n // Read again with nothing refused, for whose line it was and nothing\n // else: an option no command has is passed over, one left without its\n // value takes none, and no value read this way is used. The same table,\n // so that a value is not taken for the command it stands before.\n throw new UnreadableLine((error as Error).message, parseArgs({ args: [...argv], strict: false, options }).positionals);\n }\n const { values, positionals } = parsed;\n const asked = values.format ?? 'pretty';\n const format = asked as Options['format'];\n const formats: readonly Options['format'][] = ['pretty', 'json', 'gitlab', 'sarif', 'github'];\n if (!formats.includes(format)) throw new UnreadableLine(`--format must be pretty, json, gitlab, sarif or github, not \"${format}\"`, positionals);\n const command = positionals[0];\n // Only the commands whose findings have places print them for a forge.\n if (command !== undefined && command !== 'audit' && command !== 'premises' && format !== 'pretty' && format !== 'json') {\n throw new UnreadableLine(`--format ${format} is for audit and premises; ${command} prints pretty or json`, positionals);\n }\n const at = values.at;\n if (at !== undefined && at !== 'base' && at !== 'head' && at !== 'both') throw new UnreadableLine(`--at must be base, head or both, not \"${at}\"`, positionals);\n return {\n command,\n positionals: positionals.slice(1),\n given: Object.keys(values),\n brief: values.brief,\n base: values.base,\n root: values.root,\n format,\n strict: values.strict === true,\n help: values.help === true,\n version: values.version === true,\n write: values.write === true,\n gitHook: values['git-hook'] === true,\n paths: values.path ?? [],\n reason: values.reason,\n options: values.option ?? [],\n recommend: values.recommend,\n list: values.list === true,\n show: values.show,\n allow: values.allow === true,\n deny: values.deny === true,\n note: values.note,\n id: values.id,\n at,\n };\n}\n\n/** Everything a command needs about the repository it runs in. */\nexport interface Workspace {\n readonly root: string;\n readonly commonDir: string;\n readonly config: HarnessConfig;\n readonly siblings: Siblings;\n /** The branch checked out, or on a detached head the one the forge's CI names; `null` when neither names one. */\n readonly branch: string | null;\n /** The CI variable that named the branch, on a detached head; absent when git named it. */\n readonly branchSource?: string | undefined;\n readonly cwd: string;\n}\n\nexport async function openWorkspace(options: Pick<Options, 'root'>, io: CliIO): Promise<Workspace> {\n const cwd = options.root ?? io.cwd;\n // Each git command is a process, a few milliseconds on Linux and a few\n // hundred on a Windows workstation, and every command opens a work tree,\n // the hook before every write among them. So the top and the common\n // directory are asked for in one start of git, and where a `.git` stands\n // in the directory, as it does at the top of a work tree, the branch is\n // asked for beside them and not after. Anywhere else the branch waits, as\n // it did: below a top it would be asked twice, and in no work tree for\n // nothing.\n const [tree, early] = await Promise.all([workTree(cwd), holdsGit(cwd) ? currentBranch(cwd) : undefined]);\n const root = tree === 'unread' ? await workTreeRoot(cwd) : (tree?.root ?? null);\n if (root === null) throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);\n // The common directory and the branch are asked of the root. What git\n // said from `cwd` is that answer where `cwd` is the root, the same\n // directory asked the same thing, and nowhere else. Measured with git\n // 2.43 and 2.55: started in a work tree that `core.worktree` names, away\n // from its repository, git finds no repository, so asked of that root it\n // names no branch, where from the repository it names one. So elsewhere\n // the two are asked for from the root, as they were, at once. Only the\n // configuration can fail, so what is thrown is what it threw.\n const here = tree !== 'unread' && isTop(cwd, root) ? tree : null;\n const [common, { config }, checkedOut] = await Promise.all([here === null ? commonDirectory(root) : here.commonDir, loadConfig(root), here === null || early === undefined ? currentBranch(root) : early]);\n // Asked from the top of its own work tree, git names the common\n // directory. From a root that holds no repository, the work tree that\n // `core.worktree` names away from its own, it names none, and the\n // directory a repository would have there stands in, as it always has.\n const commonDir = common ?? `${root}/.git`;\n // A branch checked out is the branch; only a head on none, as CI checks\n // out, asks the forge which branch the run builds.\n const ci = checkedOut === null ? ciBranch(io.env) : null;\n const named = ci === null ? { branch: checkedOut } : { branch: ci.branch, branchSource: ci.source };\n return { root, commonDir, config, siblings: createSiblings(root, config), ...named, cwd: io.cwd };\n}\n\n/**\n * What one of several things asked at once answered, or what it threw,\n * thrown from here: the things are asked through `Promise.allSettled`, and\n * each is read with this where it was awaited while they were asked one\n * after another. So a run in which one of them fails still fails on the\n * first that failed in that order, whichever failed first by the clock,\n * and says what it said.\n */\nexport function answerOf<T>(asked: PromiseSettledResult<T>): T {\n if (asked.status === 'rejected') throw asked.reason;\n return asked.value;\n}\n\n/** The id the flag, the environment or the branch names, without asking spec-brief anything. */\nexport function namedId(workspace: Workspace, options: Pick<Options, 'brief'>, env: CliIO['env']): string | null {\n if (options.brief !== undefined && options.brief.trim() !== '') return options.brief.trim();\n const environment = env['SPEC_BRIEF'];\n if (environment !== undefined && environment.trim() !== '') return environment.trim();\n return briefIdFromBranch(workspace.config.branches, workspace.branch);\n}\n\nexport async function activeBrief(workspace: Workspace, options: Pick<Options, 'brief'>, env: CliIO['env']): Promise<ActiveBrief> {\n // Nothing names a brief, so spec-brief is not asked (ADR-0004): the answer\n // is none whatever the briefs are, and so the empty list is equivalent to\n // its mutant.\n if (namedId(workspace, options, env) === null) {\n return findActive([], {});\n }\n const briefs = await workspace.siblings.briefs();\n const fromBranch = briefIdFromBranch(workspace.config.branches, workspace.branch);\n return findActive(briefs, { flag: options.brief, environment: env['SPEC_BRIEF'], branch: fromBranch });\n}\n\n/**\n * What named a brief, as its caller knows it. An argument is the command\n * line's by default, the id after the command; a tool call's is its `brief`,\n * which the server says as it says every argument, by its name in quotes.\n */\nfunction namedBy(source: ActiveSource, argument: string): string {\n return source === 'argument' ? argument : `the ${source}`;\n}\n\n/** What is said of a name that names no brief spec-brief knows, by what named it. */\nexport function namesNoBrief(active: Extract<ActiveBrief, { kind: 'unknown' }>, argument = 'the argument'): string {\n return `${namedBy(active.source, argument)} names brief ${active.id}, and spec-brief knows no such brief`;\n}\n\n/**\n * The brief a command acts on, or why there is none: a note when nothing names\n * one, which a command that needs a brief refuses with, or a problem when what\n * names one is wrong. `argument` is what the caller calls an argument that\n * named it.\n */\nexport function describeActive(\n active: ActiveBrief,\n argument = 'the argument',\n): { brief: BriefRow; note: undefined; problem: null } | { brief: null; note: string; problem: null } | { brief: null; note: undefined; problem: string } {\n switch (active.kind) {\n case 'found':\n return { brief: active.brief, note: undefined, problem: null };\n case 'none':\n return { brief: null, note: active.reason, problem: null };\n case 'unknown':\n return { brief: null, note: undefined, problem: namesNoBrief(active, argument) };\n case 'archived':\n return {\n brief: null,\n note: undefined,\n problem: `${namedBy(active.source, argument)} names brief ${active.brief.id}, which is archived; a closed round writes nothing`,\n };\n }\n}\n\nexport function json(command: string, body: Record<string, unknown>): string {\n return `${JSON.stringify({ tool: 'spec-harness', version: version(), schemaVersion: JSON_SCHEMA_VERSION, command, ...body }, null, 2)}\\n`;\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What every command shares: exit codes, options, the repository a command\n * runs in, and which brief the round is working on.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { parseArgs, type ParseArgsOptionsConfig } from 'node:util';\n\nimport { findActive, type ActiveBrief, type ActiveSource } from './briefs.js';\nimport { briefIdFromBranch, ciBranch } from './branch.js';\nimport type { HarnessConfig } from './config.js';\nimport { holdsGit, isTop, loadConfig, mainWorkTree, realSpelling, repositoryPath } from './fs.js';\nimport { commonDirectory, currentBranch, workTree, workTreeRoot, type WorkTree } from './git.js';\nimport type { FindingFormat } from './formats.js';\nimport { createSiblings, type Siblings } from './siblings.js';\nimport type { BriefRow } from './types.js';\n\nexport const EXIT_OK = 0;\nexport const EXIT_FAILED = 1;\nexport const EXIT_ERROR = 2;\n\n/** The version of the JSON documents this tool prints. Bumped when a field changes meaning. */\nexport const JSON_SCHEMA_VERSION = 1;\n\nexport class UsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'UsageError';\n }\n}\n\n/**\n * A command line the parser refused, with the command and the arguments on\n * it. Whose line it was decides what the refusal is: Claude Code's hook says\n * it and holds no write for it, where every other command stops (cli.ts).\n */\nexport class UnreadableLine extends UsageError {\n readonly words: readonly string[];\n\n constructor(message: string, words: readonly string[]) {\n super(message);\n this.words = words;\n }\n}\n\nexport interface CliIO {\n readonly stdout: { write(text: string): unknown };\n readonly stderr: { write(text: string): unknown };\n readonly stdin?: () => Promise<string>;\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n}\n\nexport function version(): string {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n const manifest: { version: string } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));\n return manifest.version;\n}\n\nexport interface Options {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option given, in the order given: what tells an option left out from one a command does not read. */\n readonly given: readonly string[];\n /**\n * Every value given to each option that takes one value, by its long name,\n * in the order given. The fields below hold the last of them, which is\n * what the parser keeps.\n */\n readonly written: Readonly<Record<string, readonly string[]>>;\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n /** `gitlab`, `sarif` and `github` only for `audit` and `premises`, whose findings have places. */\n readonly format: 'pretty' | 'json' | FindingFormat;\n readonly strict: boolean;\n readonly help: boolean;\n readonly version: boolean;\n /** init: apply the plan instead of printing it. */\n readonly write: boolean;\n /** init: also install git's pre-commit hook. */\n readonly gitHook: boolean;\n /** init: `onFailure: \"block\"` on the hook entries it writes (ADR-0012). */\n readonly blockOnFailure: boolean;\n /** escalate: the files the round needs, the reason, the options and the recommendation. */\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly list: boolean;\n readonly show: string | undefined;\n /** rule: the decision and what it allows. */\n readonly allow: boolean;\n readonly deny: boolean;\n readonly note: string | undefined;\n /** probe: which probe, and at which commit. */\n readonly id: string | undefined;\n readonly at: 'base' | 'head' | 'both' | undefined;\n}\n\nexport function parseOptions(argv: readonly string[]): Options {\n // `satisfies`, where a const assertion would read the same to the compiler:\n // the sweep mutates nothing that stands under an `as`, the table with it.\n const options = {\n brief: { type: 'string' },\n base: { type: 'string' },\n root: { type: 'string' },\n format: { type: 'string' },\n strict: { type: 'boolean' },\n write: { type: 'boolean' },\n 'git-hook': { type: 'boolean' },\n 'block-on-failure': { type: 'boolean' },\n path: { type: 'string', multiple: true },\n reason: { type: 'string' },\n option: { type: 'string', multiple: true },\n recommend: { type: 'string' },\n list: { type: 'boolean' },\n show: { type: 'string' },\n allow: { type: 'boolean' },\n deny: { type: 'boolean' },\n note: { type: 'string' },\n id: { type: 'string' },\n at: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n } satisfies ParseArgsOptionsConfig;\n let parsed;\n try {\n parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options, tokens: true });\n } catch (error) {\n // Read again with nothing refused, for whose line it was and nothing\n // else: an option no command has is passed over, one left without its\n // value takes none, and no value read this way is used. The same table,\n // so that a value is not taken for the command it stands before.\n throw new UnreadableLine((error as Error).message, parseArgs({ args: [...argv], strict: false, options }).positionals);\n }\n const { values, positionals, tokens } = parsed;\n // The parser keeps the last value of an option given twice, and the earlier\n // went unread with no word of it: `--root nowhere --root .` ran in `.`. So\n // every value is kept, for the refusal of two (usage.ts). A list keeps\n // every value already.\n const table: ParseArgsOptionsConfig = options;\n const written: Record<string, string[]> = {};\n for (const token of tokens) {\n if (token.kind === 'option' && token.value !== undefined && table[token.name]!.multiple !== true) (written[token.name] ??= []).push(token.value);\n }\n // Two answers asked for at once: the version was printed, and the help went\n // unread. A line that is wrong, whoever's it is (spec-core ADR-0005). Only\n // an option's token has a name, so the test of its kind is the compiler's,\n // and its mutant is equivalent.\n const answers = tokens.flatMap((token) => (token.kind === 'option' && (token.name === 'help' || token.name === 'version') ? [token] : []));\n const other = answers.find((token) => token.name !== answers[0]!.name);\n if (other !== undefined) throw new UnreadableLine(`${answers[0]!.rawName} is given beside ${other.rawName}, and one of the two answers would go unread; give one of them`, positionals);\n const asked = values.format ?? 'pretty';\n const format = asked as Options['format'];\n const formats: readonly Options['format'][] = ['pretty', 'json', 'gitlab', 'sarif', 'github'];\n if (!formats.includes(format)) throw new UnreadableLine(`--format must be pretty, json, gitlab, sarif or github, not \"${format}\"`, positionals);\n const command = positionals[0];\n // Only the commands whose findings have places print them for a forge.\n if (command !== undefined && command !== 'audit' && command !== 'premises' && format !== 'pretty' && format !== 'json') {\n throw new UnreadableLine(`--format ${format} is for audit and premises; ${command} prints pretty or json`, positionals);\n }\n const at = values.at;\n if (at !== undefined && at !== 'base' && at !== 'head' && at !== 'both') throw new UnreadableLine(`--at must be base, head or both, not \"${at}\"`, positionals);\n return {\n command,\n positionals: positionals.slice(1),\n given: Object.keys(values),\n written,\n brief: values.brief,\n base: values.base,\n root: values.root,\n format,\n strict: values.strict === true,\n help: values.help === true,\n version: values.version === true,\n write: values.write === true,\n gitHook: values['git-hook'] === true,\n blockOnFailure: values['block-on-failure'] === true,\n paths: values.path ?? [],\n reason: values.reason,\n options: values.option ?? [],\n recommend: values.recommend,\n list: values.list === true,\n show: values.show,\n allow: values.allow === true,\n deny: values.deny === true,\n note: values.note,\n id: values.id,\n at,\n };\n}\n\n/** Everything a command needs about the repository it runs in. */\nexport interface Workspace {\n readonly root: string;\n readonly commonDir: string;\n readonly config: HarnessConfig;\n readonly siblings: Siblings;\n /** The branch checked out, or on a detached head the one the forge's CI names; `null` when neither names one. */\n readonly branch: string | null;\n /** The CI variable that named the branch, on a detached head; absent when git named it. */\n readonly branchSource?: string | undefined;\n readonly cwd: string;\n}\n\n/**\n * A work tree as the first wave of opening it found it: the directory it\n * was asked from, its top, what one start of git said from there, and the\n * branch where it was asked beside them.\n */\nexport interface FoundTree {\n readonly cwd: string;\n readonly root: string;\n readonly tree: WorkTree | 'unread';\n readonly early: string | null | undefined;\n}\n\n/** The work tree that holds `cwd`, or `null` where none does. */\nexport async function findTree(cwd: string): Promise<FoundTree | null> {\n // Each git command is a process, a few milliseconds on Linux and a few\n // hundred on a Windows workstation, and every command opens a work tree,\n // the hook before every write among them. So the top and the common\n // directory are asked for in one start of git, and where a `.git` stands\n // in the directory, as it does at the top of a work tree, the branch is\n // asked for beside them and not after. Anywhere else the branch waits, as\n // it did: below a top it would be asked twice, and in no work tree for\n // nothing.\n const [tree, early] = await Promise.all([workTree(cwd), holdsGit(cwd) ? currentBranch(cwd) : undefined]);\n if (tree === null) return null;\n const root = tree === 'unread' ? await workTreeRoot(cwd) : tree.root;\n return root === null ? null : { cwd, root, tree, early };\n}\n\n/**\n * The git directory a found work tree's repository keeps, as git names it\n * from the directory it was found from: one repository's work trees, linked\n * and main, share it, and no two repositories do. Asked again only where\n * the one start of git could not be read.\n */\nexport async function foundCommonDir(found: FoundTree): Promise<string | null> {\n // Asked again from the top, git names the directory the one start named:\n // the mutant that always asks again is equivalent, at a process's cost.\n return found.tree === 'unread' ? commonDirectory(found.root) : found.tree.commonDir;\n}\n\nexport async function openWorkspace(options: Pick<Options, 'root'>, io: CliIO, installs: Installs = MAIN_INSTALL): Promise<Workspace> {\n const cwd = options.root ?? io.cwd;\n const found = await findTree(cwd);\n if (found === null) throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);\n return openFound(found, io, installs);\n}\n\n/** The installs a work tree's siblings are looked for in after its own, given its top and its common directory (siblings.ts). */\nexport type Installs = (root: string, commonDir: string) => readonly string[];\n\n/**\n * Every command: a linked worktree with no install of its own is checked\n * with the main work tree's, where `git worktree add` leaves the project's\n * `node_modules`, as git's hook and doctor were, and as the hook is with the\n * project's (ADR-0005).\n */\nexport const MAIN_INSTALL: Installs = (root, commonDir) => {\n const main = mainWorkTree(commonDir);\n // The main work tree's own install is the one looked in first: the mutant\n // that keeps it here as well looks in it twice and finds what it found.\n return main === null || main === root ? [] : [main];\n};\n\n/** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */\nexport async function openFound(found: FoundTree, io: CliIO, installs: Installs = MAIN_INSTALL): Promise<Workspace> {\n const { cwd, root, tree, early } = found;\n // The common directory and the branch are asked of the root. What git\n // said from `cwd` is that answer where `cwd` is the root, the same\n // directory asked the same thing, and nowhere else. Measured with git\n // 2.43 and 2.55: started in a work tree that `core.worktree` names, away\n // from its repository, git finds no repository, so asked of that root it\n // names no branch, where from the repository it names one. So elsewhere\n // the two are asked for from the root, as they were, at once. Only the\n // configuration can fail, so what is thrown is what it threw.\n const here = tree !== 'unread' && isTop(cwd, root) ? tree : null;\n const [common, { config }, checkedOut] = await Promise.all([here === null ? commonDirectory(root) : here.commonDir, loadConfig(root), here === null || early === undefined ? currentBranch(root) : early]);\n // Asked from the top of its own work tree, git names the common\n // directory. From a root that holds no repository, the work tree that\n // `core.worktree` names away from its own, it names none, and the\n // directory a repository would have there stands in, as it always has.\n const commonDir = common ?? `${root}/.git`;\n // A branch checked out is the branch; only a head on none, as CI checks\n // out, asks the forge which branch the run builds.\n const ci = checkedOut === null ? ciBranch(io.env) : null;\n const named = ci === null ? { branch: checkedOut } : { branch: ci.branch, branchSource: ci.source };\n return { root, commonDir, config, siblings: createSiblings(root, config, installs(root, commonDir)), ...named, cwd: io.cwd };\n}\n\n/**\n * What one of several things asked at once answered, or what it threw,\n * thrown from here: the things are asked through `Promise.allSettled`, and\n * each is read with this where it was awaited while they were asked one\n * after another. So a run in which one of them fails still fails on the\n * first that failed in that order, whichever failed first by the clock,\n * and says what it said.\n */\nexport function answerOf<T>(asked: PromiseSettledResult<T>): T {\n if (asked.status === 'rejected') throw asked.reason;\n return asked.value;\n}\n\n/** The id the flag, the environment or the branch names, without asking spec-brief anything. */\nexport function namedId(workspace: Workspace, options: Pick<Options, 'brief'>, env: CliIO['env']): string | null {\n if (options.brief !== undefined && options.brief.trim() !== '') return options.brief.trim();\n const environment = env['SPEC_BRIEF'];\n if (environment !== undefined && environment.trim() !== '') return environment.trim();\n return briefIdFromBranch(workspace.config.branches, workspace.branch);\n}\n\nexport async function activeBrief(workspace: Workspace, options: Pick<Options, 'brief'>, env: CliIO['env']): Promise<ActiveBrief> {\n // Nothing names a brief, so spec-brief is not asked (ADR-0004): the answer\n // is none whatever the briefs are, and so the empty list is equivalent to\n // its mutant.\n if (namedId(workspace, options, env) === null) {\n return findActive([], {});\n }\n const briefs = await workspace.siblings.briefs();\n const fromBranch = briefIdFromBranch(workspace.config.branches, workspace.branch);\n return findActive(briefs, { flag: options.brief, environment: env['SPEC_BRIEF'], branch: fromBranch });\n}\n\n/**\n * What named a brief, as its caller knows it. An argument is the command\n * line's by default, the id after the command; a tool call's is its `brief`,\n * which the server says as it says every argument, by its name in quotes.\n */\nfunction namedBy(source: ActiveSource, argument: string): string {\n return source === 'argument' ? argument : `the ${source}`;\n}\n\n/** What is said of a name that names no brief spec-brief knows, by what named it. */\nexport function namesNoBrief(active: Extract<ActiveBrief, { kind: 'unknown' }>, argument = 'the argument'): string {\n return `${namedBy(active.source, argument)} names brief ${active.id}, and spec-brief knows no such brief`;\n}\n\n/**\n * The brief a command acts on, or why there is none: a note when nothing names\n * one, which a command that needs a brief refuses with, or a problem when what\n * names one is wrong. `argument` is what the caller calls an argument that\n * named it.\n */\nexport function describeActive(\n active: ActiveBrief,\n argument = 'the argument',\n): { brief: BriefRow; note: undefined; problem: null } | { brief: null; note: string; problem: null } | { brief: null; note: undefined; problem: string } {\n switch (active.kind) {\n case 'found':\n return { brief: active.brief, note: undefined, problem: null };\n case 'none':\n return { brief: null, note: active.reason, problem: null };\n case 'unknown':\n return { brief: null, note: undefined, problem: namesNoBrief(active, argument) };\n case 'archived':\n return {\n brief: null,\n note: undefined,\n problem: `${namedBy(active.source, argument)} names brief ${active.brief.id}, which is archived; a closed round writes nothing`,\n };\n }\n}\n\nexport function json(command: string, body: Record<string, unknown>): string {\n return `${JSON.stringify({ tool: 'spec-harness', version: version(), schemaVersion: JSON_SCHEMA_VERSION, command, ...body }, null, 2)}\\n`;\n}\n\n/**\n * Whether a work tree other than the one whose top is `top` may hold `cwd`:\n * `cwd` is outside it, or a `.git` stands in a directory between the two,\n * where a linked worktree, a submodule or a repository of its own starts.\n * Where neither is so, `cwd` is in that work tree, and git is asked nothing\n * more: a session works at its project's top, or below it, for nearly every\n * write. Answering that one may is what git is then asked, so the mutant\n * that always answers so is equivalent, at the cost of a process, and so\n * is each that only answers so more often: a slice one short reaches the\n * top itself, which holds a `.git`, and a part named in place of none is\n * a directory no top holds.\n */\nexport function mayBeElsewhere(cwd: string, top: string): boolean {\n const path = repositoryPath(cwd, top, cwd);\n if (path === null) return true;\n const parts = path === '' ? [] : path.split('/');\n return parts.some((_, index) => holdsGit(join(top, ...parts.slice(0, index + 1))));\n}\n\n/**\n * The work tree that holds `cwd`, where it is another work tree of the\n * repository the work tree `named` is one of, as the hook's root names it, by the git directory the two share;\n * `null` where `cwd` is in the root's own, in another repository, or in\n * none (ADR-0005).\n */\nexport async function otherTree(named: Workspace, cwd: string): Promise<FoundTree | null> {\n // Asked anyway, git names the root's own top for a directory in its work\n // tree, and none for one not made, either of which answers `null` below:\n // the mutant that always asks is equivalent, at a process's cost.\n if (!mayBeElsewhere(cwd, named.root)) return null;\n const found = await findTree(cwd);\n // The project's own work tree, found again, would decide every path the\n // same way once more: the check saves reading its configuration twice, and\n // its mutant is equivalent.\n if (found === null || found.root === named.root) return null;\n const common = await foundCommonDir(found);\n // git names a common directory wherever it names a top, so `null` is\n // there for the type.\n if (common === null || realSpelling(common) !== realSpelling(named.commonDir)) return null;\n return found;\n}\n\n/**\n * The top of the nearest work tree, of any repository, above a directory or\n * a file: the nearest directory above it that holds a `.git`, found with no\n * process; `null` where none does. A path not yet written has the\n * directories it would be written in looked through, those not made yet\n * among them.\n */\nfunction topAbove(path: string): string | null {\n for (let directory = dirname(path); ; directory = dirname(directory)) {\n if (holdsGit(directory)) return directory;\n // Only the top of the filesystem is its own parent.\n if (dirname(directory) === directory) return null;\n }\n}\n\n/**\n * The innermost work tree of the repository the work tree `named` is one of\n * that holds a path, where it is not `named` itself: a linked worktree or the\n * main one, inside the project, beside it, or around it (ADR-0005). The\n * directories above the path are looked through, with no process, for each\n * that holds a `.git`; git is asked of each that is not `named`'s top, and\n * one that git names as no work tree of this repository's, a submodule's, a\n * clone's inside the tree, or a `.git` that stands in no work tree's top, is\n * passed by for the next above it. `null` where the first of this repository\n * met is `named`, or none is: the path is decided by `named`, as before, a\n * path in no work tree of the repository among them.\n */\nasync function treeHolding(named: Workspace, path: string, from: string): Promise<FoundTree | null> {\n const own = realSpelling(named.root);\n // An absolute path, as Claude Code sends one, is what resolve leaves it.\n for (let top = topAbove(realSpelling(resolve(from, path))); top !== null; top = topAbove(top)) {\n if (top === own) return null;\n const tree = await otherTree(named, top);\n // git names a work tree whose top is not this directory where it reads\n // the `.git` here as no repository's, an empty directory say, and names\n // the one around it: the walk meets that one's top next, past any `.git`\n // between that git reads as none too, and takes it. So the mutant that\n // takes what git names here is equivalent for every work tree git makes;\n // the check keeps a `core.worktree` set by hand to name another\n // directory from deciding a path that directory does not hold.\n if (tree !== null && realSpelling(tree.root) === top) return tree;\n }\n return null;\n}\n\n/**\n * The work trees a write is decided in, each with the paths it decides, for\n * the hook and for the server's `check_path` (ADR-0005). The repository is\n * the one the hook's root names, as before. Each path is decided by the\n * innermost work tree of that repository that holds it, by its branch, its\n * briefs and its configuration, read from its top, with its siblings looked\n * for in its install and then the project's: there a session works in a\n * worktree while Claude Code names the project it started in, or writes from\n * one into another, and the hook decided by the project's branch, which\n * named no brief, so a protected write passed with no word. A path in no\n * other work tree of the repository is decided by the work tree the root\n * names, as before: a write to the project's own file from a worktree is\n * refused where it was, and a path in another repository, or in none, is\n * read as it was. `cwd` is what a relative path is read from.\n */\nexport async function writesBy(named: Workspace, cwd: string | null, paths: readonly string[], io: CliIO): Promise<{ readonly workspace: Workspace; readonly paths: readonly string[] }[]> {\n const from = cwd ?? io.cwd;\n const trees: { readonly tree: FoundTree | null; readonly paths: string[] }[] = [];\n // The parts in the order their work trees are first met, each with its\n // paths in the order they were named.\n for (const path of paths) {\n const tree = await treeHolding(named, path, from);\n const part = trees.find((other) => other.tree?.root === tree?.root);\n if (part === undefined) trees.push({ tree, paths: [path] });\n else part.paths.push(path);\n }\n // The root's own last, as it was decided after the work tree Claude works in.\n const ordered = [...trees.filter((part) => part.tree !== null), ...trees.filter((part) => part.tree === null)];\n return Promise.all(ordered.map(async ({ tree, paths: held }) => ({ workspace: tree === null ? named : await openFound(tree, io, () => [named.root]), paths: held })));\n}\n"]}
|
package/hooks/hooks.json
CHANGED
|
@@ -33,6 +33,22 @@
|
|
|
33
33
|
}
|
|
34
34
|
]
|
|
35
35
|
}
|
|
36
|
+
],
|
|
37
|
+
"SessionStart": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node",
|
|
43
|
+
"args": [
|
|
44
|
+
"${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js",
|
|
45
|
+
"hook",
|
|
46
|
+
"claude"
|
|
47
|
+
],
|
|
48
|
+
"timeout": 60
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
36
52
|
]
|
|
37
53
|
}
|
|
38
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descent-vtt/spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "The agent-facing member of the spec-* tools: context for a round, a guard on every write, an audit when it ends, probes that prove a defect before it is filed, and rulings a person signs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@descent-vtt/spec-brief": "^0.8.0",
|
|
77
|
-
"@descent-vtt/spec-guard": "^0.
|
|
77
|
+
"@descent-vtt/spec-guard": "^0.26.0",
|
|
78
78
|
"@stryker-mutator/core": "^10.0.0",
|
|
79
79
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
80
80
|
"@types/node": "^22.20.4",
|