@descent-vtt/spec-harness 0.19.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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +212 -0
  4. package/README.md +30 -15
  5. package/dist/briefs.d.ts +45 -0
  6. package/dist/briefs.js +73 -0
  7. package/dist/briefs.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +211 -134
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +7 -2
  12. package/dist/commands.js.map +1 -1
  13. package/dist/config.d.ts +13 -1
  14. package/dist/config.js +14 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/configure.d.ts +55 -1
  17. package/dist/configure.js +130 -22
  18. package/dist/configure.js.map +1 -1
  19. package/dist/fs.d.ts +78 -1
  20. package/dist/fs.js +289 -10
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +50 -2
  23. package/dist/git.js +98 -8
  24. package/dist/git.js.map +1 -1
  25. package/dist/hooks.d.ts +45 -0
  26. package/dist/hooks.js +62 -3
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/round.d.ts +8 -0
  29. package/dist/round.js +23 -8
  30. package/dist/round.js.map +1 -1
  31. package/dist/sandbox.js +4 -3
  32. package/dist/sandbox.js.map +1 -1
  33. package/dist/server.d.ts +25 -1
  34. package/dist/server.js +194 -27
  35. package/dist/server.js.map +1 -1
  36. package/dist/setup.js +40 -10
  37. package/dist/setup.js.map +1 -1
  38. package/dist/siblings.d.ts +24 -3
  39. package/dist/siblings.js +106 -14
  40. package/dist/siblings.js.map +1 -1
  41. package/dist/usage.d.ts +42 -7
  42. package/dist/usage.js +65 -6
  43. package/dist/usage.js.map +1 -1
  44. package/dist/workspace.d.ts +45 -3
  45. package/dist/workspace.js +133 -13
  46. package/dist/workspace.js.map +1 -1
  47. package/package.json +1 -1
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, mainWorkTree } 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 },
@@ -119,6 +121,7 @@ export function parseOptions(argv) {
119
121
  version: values.version === true,
120
122
  write: values.write === true,
121
123
  gitHook: values['git-hook'] === true,
124
+ blockOnFailure: values['block-on-failure'] === true,
122
125
  paths: values.path ?? [],
123
126
  reason: values.reason,
124
127
  options: values.option ?? [],
@@ -159,7 +162,7 @@ export async function foundCommonDir(found) {
159
162
  // the mutant that always asks again is equivalent, at a process's cost.
160
163
  return found.tree === 'unread' ? commonDirectory(found.root) : found.tree.commonDir;
161
164
  }
162
- export async function openWorkspace(options, io, installs = NO_INSTALLS) {
165
+ export async function openWorkspace(options, io, installs = MAIN_INSTALL) {
163
166
  const cwd = options.root ?? io.cwd;
164
167
  const found = await findTree(cwd);
165
168
  if (found === null)
@@ -167,16 +170,10 @@ export async function openWorkspace(options, io, installs = NO_INSTALLS) {
167
170
  return openFound(found, io, installs);
168
171
  }
169
172
  /**
170
- * Every command but the hooks and doctor: a work tree is checked with its
171
- * own install, as it was. A name put on the list is a directory relative to
172
- * where the command runs, which holds no install, and no list at all is
173
- * the siblings' own default of none, so those mutants are equivalent.
174
- */
175
- const NO_INSTALLS = () => [];
176
- /**
177
- * git's hook and doctor: a linked worktree with no install of its own is
178
- * checked with the main work tree's, where `git worktree add` leaves the
179
- * project's `node_modules` (ADR-0005).
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).
180
177
  */
181
178
  export const MAIN_INSTALL = (root, commonDir) => {
182
179
  const main = mainWorkTree(commonDir);
@@ -185,7 +182,7 @@ export const MAIN_INSTALL = (root, commonDir) => {
185
182
  return main === null || main === root ? [] : [main];
186
183
  };
187
184
  /** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */
188
- export async function openFound(found, io, installs = NO_INSTALLS) {
185
+ export async function openFound(found, io, installs = MAIN_INSTALL) {
189
186
  const { cwd, root, tree, early } = found;
190
187
  // The common directory and the branch are asked of the root. What git
191
188
  // said from `cwd` is that answer where `cwd` is the root, the same
@@ -278,4 +275,127 @@ export function describeActive(active, argument = 'the argument') {
278
275
  export function json(command, body) {
279
276
  return `${JSON.stringify({ tool: 'spec-harness', version: version(), schemaVersion: JSON_SCHEMA_VERSION, command, ...body }, null, 2)}\n`;
280
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
+ }
281
401
  //# sourceMappingURL=workspace.js.map
@@ -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, mainWorkTree } 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 /** 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, 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 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 = NO_INSTALLS): 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 but the hooks and doctor: a work tree is checked with its\n * own install, as it was. A name put on the list is a directory relative to\n * where the command runs, which holds no install, and no list at all is\n * the siblings' own default of none, so those mutants are equivalent.\n */\nconst NO_INSTALLS: Installs = () => [];\n\n/**\n * git's hook and doctor: a linked worktree with no install of its own is\n * checked with the main work tree's, where `git worktree add` leaves the\n * project's `node_modules` (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 = NO_INSTALLS): 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"]}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descent-vtt/spec-harness",
3
- "version": "0.19.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",