@descent-vtt/spec-harness 0.13.0 → 0.15.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 (52) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/README.md +18 -3
  5. package/dist/answers.d.ts +6 -0
  6. package/dist/answers.js +12 -1
  7. package/dist/answers.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +81 -43
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +1 -0
  12. package/dist/commands.js.map +1 -1
  13. package/dist/configure.d.ts +58 -2
  14. package/dist/configure.js +116 -3
  15. package/dist/configure.js.map +1 -1
  16. package/dist/context.d.ts +16 -7
  17. package/dist/context.js +36 -11
  18. package/dist/context.js.map +1 -1
  19. package/dist/fs.js +1 -0
  20. package/dist/fs.js.map +1 -1
  21. package/dist/git.js +2 -1
  22. package/dist/git.js.map +1 -1
  23. package/dist/hooks.js.map +1 -1
  24. package/dist/host.js.map +1 -1
  25. package/dist/literal.d.ts +18 -0
  26. package/dist/literal.js +21 -0
  27. package/dist/literal.js.map +1 -0
  28. package/dist/probe.js +1 -1
  29. package/dist/probe.js.map +1 -1
  30. package/dist/reader.js +2 -0
  31. package/dist/reader.js.map +1 -1
  32. package/dist/round.d.ts +12 -1
  33. package/dist/round.js +23 -7
  34. package/dist/round.js.map +1 -1
  35. package/dist/sandbox.js.map +1 -1
  36. package/dist/server.js +2 -1
  37. package/dist/server.js.map +1 -1
  38. package/dist/setup.js +2 -1
  39. package/dist/setup.js.map +1 -1
  40. package/dist/siblings.js +2 -1
  41. package/dist/siblings.js.map +1 -1
  42. package/dist/signers.js +5 -0
  43. package/dist/signers.js.map +1 -1
  44. package/dist/usage.d.ts +83 -27
  45. package/dist/usage.js +61 -16
  46. package/dist/usage.js.map +1 -1
  47. package/dist/versions.js +3 -0
  48. package/dist/versions.js.map +1 -1
  49. package/dist/workspace.d.ts +9 -0
  50. package/dist/workspace.js +48 -32
  51. package/dist/workspace.js.map +1 -1
  52. package/package.json +3 -3
package/dist/usage.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * `null`. The one thing only the disk knows, whether `--root` is a
14
14
  * directory, is asked of the caller.
15
15
  */
16
+ import { literal } from './literal.js';
16
17
  /** Whether a value names nothing: empty, or only space. */
17
18
  export function isBlank(value) {
18
19
  return value.trim() === '';
@@ -26,7 +27,7 @@ const one = (value) => (value === undefined ? [] : [value]);
26
27
  * parser refuses any other, the empty one with them. Nor is `--note`, which
27
28
  * `rule` refuses by name itself.
28
29
  */
29
- const VALUED = {
30
+ const VALUED = literal({
30
31
  brief: { of: (line) => one(line.brief), says: "names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names" },
31
32
  base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },
32
33
  root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },
@@ -36,7 +37,7 @@ const VALUED = {
36
37
  recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },
37
38
  show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },
38
39
  id: { of: (line) => one(line.id), says: "names no probe; give it a probe's id, or leave it out to run every probe" },
39
- };
40
+ });
40
41
  /**
41
42
  * The line for a value that names nothing. `subject` is the input as its
42
43
  * caller knows it: `--brief` on a command line, `"brief"` in a tool call. The
@@ -45,11 +46,11 @@ const VALUED = {
45
46
  export function emptyValue(subject, option, value) {
46
47
  return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;
47
48
  }
48
- const NO_ARGUMENT = { limit: { most: 0, takes: 'no argument' } };
49
- const ONE_BRIEF = {
49
+ const NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });
50
+ const ONE_BRIEF = literal({
50
51
  limit: { most: 1, takes: "one argument at most, a brief's id" },
51
52
  argument: { role: 'the brief', instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names", option: 'brief' },
52
- };
53
+ });
53
54
  /**
54
55
  * Every command, with what it reads of a command line. An option is listed
55
56
  * for a command only where the command's answer depends on it: `init`
@@ -57,7 +58,7 @@ const ONE_BRIEF = {
57
58
  * neither takes `--base` or `--brief`, and taking them would tell someone
58
59
  * the base or the brief had been set.
59
60
  */
60
- const SHAPE = {
61
+ const SHAPE = literal({
61
62
  context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },
62
63
  guard: {
63
64
  options: ['brief', 'base', 'root', 'format', 'strict'],
@@ -90,7 +91,7 @@ const SHAPE = {
90
91
  // that acts on it stops: a brief or a base that names nothing is a line of
91
92
  // doctor's report.
92
93
  doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },
93
- };
94
+ });
94
95
  // A map, since a command is a word a person types and an object answers to
95
96
  // more names than it was given: `constructor` has no shape.
96
97
  const SHAPES = new Map(Object.entries(SHAPE));
@@ -102,20 +103,64 @@ function listed(options) {
102
103
  return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
103
104
  }
104
105
  /**
105
- * Whether the line is Claude Code's hook. It is exempt from every refusal
106
- * here, and read as each release since 0.1.0 read it: the line lives in
107
- * settings files and in the plugin of every release, which this one cannot
108
- * rewrite, and exit 2 from it holds every write of a session, the write that
109
- * would fix the line among them. An option the parser itself does not know
110
- * is exit 2 there, as it always was.
106
+ * Whether a line is Claude Code's hook, by its command and the argument
107
+ * after it. It is exempt from every refusal here, and read as each release
108
+ * since 0.1.0 read it: the line lives in settings files and in the plugin of
109
+ * every release, which this one cannot rewrite, and exit 2 from it holds
110
+ * every write of a session, the write that would fix the line among them.
111
+ * A line the parser cannot read is asked the same, and the hook says it
112
+ * with exit 1 (cli.ts).
111
113
  */
112
- function isClaudeHook(line) {
113
- return line.command === 'hook' && line.positionals[0] === 'claude';
114
+ export function isClaudeHook(command, argument) {
115
+ return command === 'hook' && argument === 'claude';
114
116
  }
115
117
  /** The words for a `--root` that is a file, or is not there, wherever it is said. */
116
118
  export function noDirectory(root) {
117
119
  return `--root is "${root}", which is not a directory`;
118
120
  }
121
+ /**
122
+ * A line the parser refused: an option no command has, an option left
123
+ * without its value, a `--format` or an `--at` that is none. The parser's
124
+ * words are kept, on one line: what node says of an option that may have
125
+ * lost its value is three.
126
+ */
127
+ export function unreadHookLine(said) {
128
+ return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\n').join(' ')})`, mend: 'mend it in Claude Code\'s settings, where init writes "hook claude" and nothing after it' } };
129
+ }
130
+ /**
131
+ * A line that parsed, by the root it gives. An empty root is the directory
132
+ * the hook was started in: git starts there when it is handed no directory,
133
+ * so the hook has guarded there for an empty root in every release. A root
134
+ * that is a file, or is not there, is one git starts in neither of, which
135
+ * read as a session outside a work tree, exit 0 and no word, and the guard
136
+ * someone had configured was off.
137
+ */
138
+ export function readHookLine(root, cwd, isDirectory) {
139
+ const given = root === '' ? cwd : root;
140
+ if (given !== undefined && !isDirectory(given)) {
141
+ return { state: 'off', fault: { wrong: noDirectory(given), mend: "give the hook's line a directory that exists, or leave --root out for the project Claude Code names" } };
142
+ }
143
+ return { state: 'guards', root: given };
144
+ }
145
+ /**
146
+ * What Claude Code's hook does with the words it is handed, or `null` for
147
+ * words that are not its line: read by the parser the command line reads
148
+ * every line with, and known by its command and the argument after it on a
149
+ * line that parsed and on one that did not, as the command line knows it
150
+ * (cli.ts). `parse` is that parser, which throws its refusal with the words
151
+ * it could still read.
152
+ */
153
+ export function claudeHookLine(words, parse, cwd, isDirectory) {
154
+ let line;
155
+ try {
156
+ line = parse(words);
157
+ }
158
+ catch (error) {
159
+ const refused = error;
160
+ return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;
161
+ }
162
+ return isClaudeHook(line.command, line.positionals[0]) ? readHookLine(line.root, cwd, isDirectory) : null;
163
+ }
119
164
  /**
120
165
  * Why the command line is refused, or `null` when the command can run:
121
166
  * an option it does not read, alone or beside another, an argument it does
@@ -131,7 +176,7 @@ export function refusal(line, isDirectory) {
131
176
  if (command === undefined)
132
177
  return null;
133
178
  const shape = SHAPES.get(command);
134
- if (shape === undefined || isClaudeHook(line))
179
+ if (shape === undefined || isClaudeHook(command, positionals[0]))
135
180
  return null;
136
181
  const stray = line.given.find((option) => !shape.options.includes(option));
137
182
  if (stray !== undefined)
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// An option left out has no value to refuse, and a value that names something\n// is passed over as none is: the empty list is equivalent to its mutant.\nconst one = (value: string | undefined): readonly string[] => (value === undefined ? [] : [value]);\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = {\n brief: { of: (line) => one(line.brief), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: (line) => one(line.reason), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: (line) => one(line.id), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n} as const satisfies Record<string, Valued>;\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = { limit: { most: 0, takes: 'no argument' } } as const;\nconst ONE_BRIEF = {\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n} as const;\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = {\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n} as const satisfies Record<string, Shape>;\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1) as string}`;\n}\n\n/**\n * Whether the line is Claude Code's hook. It is exempt from every refusal\n * here, and read as each release since 0.1.0 read it: the line lives in\n * settings files and in the plugin of every release, which this one cannot\n * rewrite, and exit 2 from it holds every write of a session, the write that\n * would fix the line among them. An option the parser itself does not know\n * is exit 2 there, as it always was.\n */\nfunction isClaudeHook(line: CommandLine): boolean {\n return line.command === 'hook' && line.positionals[0] === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known and neither\n * `--help` nor `--version` answers instead; a command with no shape is none,\n * which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(line)) return null;\n const stray = line.given.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => line.given.includes(candidate.option));\n if (mode !== undefined) {\n const unread = line.given.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAmBvC,2DAA2D;AAC3D,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAQD,8EAA8E;AAC9E,yEAAyE;AACzE,MAAM,GAAG,GAAG,CAAC,KAAyB,EAAqB,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAEnG;;;;;GAKG;AACH,MAAM,MAAM,GAAG,OAAO,CAAC;IACrB,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,kGAAkG,EAAE;IAClJ,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,wFAAwF,EAAE;IACtI,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kFAAkF,EAAE;IAChI,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,2CAA2C,EAAE;IACrF,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,oEAAoE,EAAE;IACtH,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,6DAA6D,EAAE;IAC3G,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,+DAA+D,EAAE;IACvH,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iFAAiF,EAAE;IAC/H,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,0EAA0E,EAAE;CACrH,CAAkC,CAAC;AAIpC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,MAAoB,EAAE,KAAa;IAC7E,OAAO,GAAG,OAAO,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;AAChF,CAAC;AAwBD,MAAM,WAAW,GAAG,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAC1E,MAAM,SAAS,GAAG,OAAO,CAAC;IACxB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE;IAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,2FAA2F,EAAE,MAAM,EAAE,OAAO,EAAE;CACvJ,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,KAAK,GAAG,OAAO,CAAC;IACpB,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IACvE,KAAK,EAAE;QACL,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;QACtD,KAAK,EAAE,IAAI;QACX,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,yCAAyC,EAAE;KACjF;IACD,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,iCAAiC,EAAE,EAAE;IAC1G,0EAA0E;IAC1E,0EAA0E;IAC1E,QAAQ,EAAE;QACR,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC;QAC7F,GAAG,WAAW;QACd,KAAK,EAAE;YACL,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;YAC9C,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;SAC/C;KACF;IACD,IAAI,EAAE;QACJ,OAAO,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC;QACpD,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,mCAAmC,EAAE;QAC9D,QAAQ,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,4DAA4D,EAAE;KAC5G;IACD,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IACvE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,SAAS,EAAE;IAC/E,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,SAAS,EAAE;IACjF,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,WAAW,EAAE;IAC5E,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,GAAG,WAAW,EAAE;IAC1E,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,WAAW,EAAE;IAC1C,yEAAyE;IACzE,2EAA2E;IAC3E,mBAAmB;IACnB,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,WAAW,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE;CAC/G,CAAiC,CAAC;AAKnC,2EAA2E;AAC3E,4DAA4D;AAC5D,MAAM,MAAM,GAA+B,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAE1E,SAAS,MAAM,CAAC,OAA0B;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;IACrD,mEAAmE;IACnE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,sBAAsB,KAAK,CAAC,CAAC,CAAW,EAAE,CAAC;IAC1E,OAAO,mBAAmB,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAA2B,EAAE,QAA4B;IACpF,OAAO,OAAO,KAAK,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC;AACrD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,cAAc,IAAI,6BAA6B,CAAC;AACzD,CAAC;AAuBD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,mCAAmC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,0FAA0F,EAAE,EAAE,CAAC;AAChN,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAwB,EAAE,GAAW,EAAE,WAAsC;IACxG,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,qGAAqG,EAAE,EAAE,CAAC;IAC7K,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAwB,EACxB,KAA+C,EAC/C,GAAW,EACX,WAAsC;IAEtC,IAAI,IAAiB,CAAC;IACtB,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAwE,CAAC;QACzF,OAAO,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnG,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5G,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,IAAiB,EAAE,WAAsC;IAC/E,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACtC,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS,IAAI,YAAY,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3E,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,GAAG,OAAO,oBAAoB,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;IAChG,iEAAiE;IACjE,sEAAsE;IACtE,gDAAgD;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACrF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACpG,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,GAAG,OAAO,MAAM,IAAI,CAAC,MAAM,oBAAoB,MAAM,cAAc,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3I,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAClE,OAAO,GAAG,OAAO,UAAU,KAAK,CAAC,KAAK,CAAC,KAAK,SAAS,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5H,CAAC;IACD,yEAAyE;IACzE,sEAAsE;IACtE,kBAAkB;IAClB,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAC3B,IAAI,QAAQ,EAAE,MAAM,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACrG,OAAO,GAAG,OAAO,cAAc,QAAQ,CAAC,IAAI,2BAA2B,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC;IACjM,CAAC;IACD,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAA6B,EAAE,CAAC;QAClF,IAAI,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC;YAAE,SAAS;QAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,UAAU,CAAC,KAAK,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxD,OAAO,GAAG,OAAO,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,uBAAuB,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChI,CAAC;IACD,4EAA4E;IAC5E,uEAAuE;IACvE,yEAAyE;IACzE,oDAAoD;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvD,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,4EAA4E,CAAC;IAC/G,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\nimport { literal } from './literal.js';\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// An option left out has no value to refuse, and a value that names something\n// is passed over as none is: the empty list is equivalent to its mutant.\nconst one = (value: string | undefined): readonly string[] => (value === undefined ? [] : [value]);\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = literal({\n brief: { of: (line) => one(line.brief), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: (line) => one(line.reason), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: (line) => one(line.id), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n}) satisfies Record<string, Valued>;\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });\nconst ONE_BRIEF = literal({\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n});\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = literal({\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n}) satisfies Record<string, Shape>;\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)!}`;\n}\n\n/**\n * Whether a line is Claude Code's hook, by its command and the argument\n * after it. It is exempt from every refusal here, and read as each release\n * since 0.1.0 read it: the line lives in settings files and in the plugin of\n * every release, which this one cannot rewrite, and exit 2 from it holds\n * every write of a session, the write that would fix the line among them.\n * A line the parser cannot read is asked the same, and the hook says it\n * with exit 1 (cli.ts).\n */\nexport function isClaudeHook(command: string | undefined, argument: string | undefined): boolean {\n return command === 'hook' && argument === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/** What is wrong with the line of Claude Code's hook, and what mends it. */\nexport interface LineFault {\n readonly wrong: string;\n readonly mend: string;\n}\n\n/**\n * What Claude Code's hook does with its own line, as far as the line decides\n * it: it guards, in the root the line gives it where it gives one, or the\n * guard is off, for a fault the hook says with exit 1 and holds no write for\n * (ADR-0005). The hook reads its line through the three functions below, and\n * `doctor` reads a line it finds in Claude Code's settings through the same\n * three, so what it says of a line is what the hook does with it.\n */\nexport type HookLine = { readonly state: 'guards'; readonly root: string | undefined } | GuardOff;\n\nexport interface GuardOff {\n readonly state: 'off';\n readonly fault: LineFault;\n}\n\n/**\n * A line the parser refused: an option no command has, an option left\n * without its value, a `--format` or an `--at` that is none. The parser's\n * words are kept, on one line: what node says of an option that may have\n * lost its value is three.\n */\nexport function unreadHookLine(said: string): GuardOff {\n return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\\n').join(' ')})`, mend: 'mend it in Claude Code\\'s settings, where init writes \"hook claude\" and nothing after it' } };\n}\n\n/**\n * A line that parsed, by the root it gives. An empty root is the directory\n * the hook was started in: git starts there when it is handed no directory,\n * so the hook has guarded there for an empty root in every release. A root\n * that is a file, or is not there, is one git starts in neither of, which\n * read as a session outside a work tree, exit 0 and no word, and the guard\n * someone had configured was off.\n */\nexport function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): HookLine {\n const given = root === '' ? cwd : root;\n if (given !== undefined && !isDirectory(given)) {\n return { state: 'off', fault: { wrong: noDirectory(given), mend: \"give the hook's line a directory that exists, or leave --root out for the project Claude Code names\" } };\n }\n return { state: 'guards', root: given };\n}\n\n/**\n * What Claude Code's hook does with the words it is handed, or `null` for\n * words that are not its line: read by the parser the command line reads\n * every line with, and known by its command and the argument after it on a\n * line that parsed and on one that did not, as the command line knows it\n * (cli.ts). `parse` is that parser, which throws its refusal with the words\n * it could still read.\n */\nexport function claudeHookLine(\n words: readonly string[],\n parse: (argv: readonly string[]) => CommandLine,\n cwd: string,\n isDirectory: (path: string) => boolean,\n): HookLine | null {\n let line: CommandLine;\n try {\n line = parse(words);\n } catch (error) {\n const refused = error as { readonly message: string; readonly words: readonly string[] };\n return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;\n }\n return isClaudeHook(line.command, line.positionals[0]) ? readHookLine(line.root, cwd, isDirectory) : null;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known and neither\n * `--help` nor `--version` answers instead; a command with no shape is none,\n * which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(command, positionals[0])) return null;\n const stray = line.given.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => line.given.includes(candidate.option));\n if (mode !== undefined) {\n const unread = line.given.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
package/dist/versions.js CHANGED
@@ -80,6 +80,9 @@ export function checkClaudeCode(answer) {
80
80
  const said = answer.output.trim().split('\n')[0].trim();
81
81
  if (said === '')
82
82
  return { state: 'unknown', reason: 'claude --version printed nothing' };
83
+ // What was said is trimmed and not empty, so its first part ends at the
84
+ // first space, whether the pattern takes that one or the run of them: one
85
+ // is equivalent to its mutant.
83
86
  const first = said.split(/\s+/)[0];
84
87
  const parsed = parseVersion(first);
85
88
  if (parsed === null)
@@ -1 +1 @@
1
- {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The oldest release of each sibling this harness runs, and an installed\n * sibling measured against it.\n *\n * An older sibling is not run. It would answer, and the answer would lack what\n * this release relies on: spec-brief before 0.2.0 has no `waive` hook, so its\n * archive refuses a protected file a signed ruling allows. It is reported with\n * the minimum instead, as a missing sibling is reported (ADR-0002, ADR-0011).\n *\n * package.json says the same minimums to npm as `peerDependencies`; a test\n * holds the two together. So is the oldest Claude Code that runs the hooks,\n * which doctor measures the one on `PATH` against.\n */\n\nimport type { SiblingName } from './config.js';\n\nexport const MINIMUM_VERSIONS: Readonly<Record<SiblingName, string>> = {\n // The plugin `waive` hook, through which spec-brief's archive accepts a\n // protected file that a signed ruling allows.\n 'spec-brief': '0.2.0',\n // An archived brief read as a record, as init configures it, where an older\n // release reads it as retired and what depends on it as a stale premise.\n 'spec-graph': '0.9.0',\n // Scopes read with the spec-core globs the harness reads them with, and an\n // archived brief's assertions withheld rather than run.\n 'spec-guard': '0.12.0',\n};\n\nexport interface Version {\n readonly major: number;\n readonly minor: number;\n readonly patch: number;\n readonly prerelease: boolean;\n}\n\n/** A semver version, or `null`. Build metadata is accepted and ignored, as precedence ignores it. */\nexport function parseVersion(text: string): Version | null {\n const match = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/.exec(text);\n if (match === null) return null;\n return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] !== undefined };\n}\n\n/**\n * Whether `installed` is `minimum` or later by semver precedence. A minimum is\n * a release, never a prerelease, so a prerelease of the minimum comes before\n * it and a prerelease of anything later comes after it.\n */\nexport function meets(installed: Version, minimum: Version): boolean {\n const order = [installed.major - minimum.major, installed.minor - minimum.minor, installed.patch - minimum.patch].find((difference) => difference !== 0) ?? 0;\n return order > 0 || (order === 0 && !installed.prerelease);\n}\n\nexport type VersionCheck =\n | { readonly ok: true; readonly version: string }\n | { readonly ok: false; readonly version: string | null; readonly reason: string };\n\n/**\n * The `version` an installed sibling's package.json declares, against the\n * minimum. A version that cannot be read cannot be shown to meet it, and is\n * refused as an old one is.\n */\nexport function checkVersion(name: SiblingName, declared: unknown): VersionCheck {\n const minimum = MINIMUM_VERSIONS[name];\n const needs = `spec-harness needs ${minimum} or later: npm install --save-dev @descent-vtt/${name}@latest`;\n if (typeof declared !== 'string') return { ok: false, version: null, reason: `the ${name} installed here declares no version; ${needs}` };\n const parsed = parseVersion(declared);\n if (parsed === null) return { ok: false, version: declared, reason: `the ${name} installed here declares \"${declared}\", which is not a version; ${needs}` };\n if (meets(parsed, parseVersion(minimum) as Version)) return { ok: true, version: declared };\n return { ok: false, version: declared, reason: `${name} ${declared} is installed here; ${needs}` };\n}\n\n/**\n * The oldest Claude Code that runs the guard hooks (ADR-0012). Its changelog\n * adds hook `args` in 2.1.139. An older release runs the hook's bare\n * `command`, `node`, which reads the hook's input as a script and fails, and\n * a PreToolUse hook that fails with anything but exit 2 blocks nothing: every\n * write passes unguarded.\n */\nexport const CLAUDE_CODE_MINIMUM = '2.1.139';\n\n/**\n * What `claude --version` printed; the version the package.json of the\n * Claude Code a Windows shim runs declares, which is `file`; or why neither\n * could be had.\n */\nexport type ClaudeCodeAnswer = { readonly output: string } | { readonly declared: string; readonly file: string } | { readonly missing: string };\n\nexport type ClaudeCodeCheck =\n /** `file` is the package.json the version was read from, when a shim was read rather than claude run. */\n | { readonly state: 'ok' | 'outdated'; readonly version: string; readonly file?: string }\n /** Never read as fine: an older release lets every write through. */\n | { readonly state: 'unknown'; readonly reason: string };\n\n/**\n * The release `claude --version` names in its first word, `2.1.235 (Claude\n * Code)`, or the one a package.json declares, against the minimum.\n */\nexport function checkClaudeCode(answer: ClaudeCodeAnswer): ClaudeCodeCheck {\n if ('missing' in answer) return { state: 'unknown', reason: answer.missing };\n if ('declared' in answer) {\n const declared = parseVersion(answer.declared);\n if (declared === null) return { state: 'unknown', reason: `${answer.file} declares \"${answer.declared}\", which is not a version` };\n return { state: meets(declared, parseVersion(CLAUDE_CODE_MINIMUM) as Version) ? 'ok' : 'outdated', version: answer.declared, file: answer.file };\n }\n // split() gives at least one string, so the first line is always there.\n const said = (answer.output.trim().split('\\n')[0] as string).trim();\n if (said === '') return { state: 'unknown', reason: 'claude --version printed nothing' };\n const first = said.split(/\\s+/)[0] as string;\n const parsed = parseVersion(first);\n if (parsed === null) return { state: 'unknown', reason: `claude --version printed \"${said}\", which is not a version` };\n return { state: meets(parsed, parseVersion(CLAUDE_CODE_MINIMUM) as Version) ? 'ok' : 'outdated', version: first };\n}\n"]}
1
+ {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The oldest release of each sibling this harness runs, and an installed\n * sibling measured against it.\n *\n * An older sibling is not run. It would answer, and the answer would lack what\n * this release relies on: spec-brief before 0.2.0 has no `waive` hook, so its\n * archive refuses a protected file a signed ruling allows. It is reported with\n * the minimum instead, as a missing sibling is reported (ADR-0002, ADR-0011).\n *\n * package.json says the same minimums to npm as `peerDependencies`; a test\n * holds the two together. So is the oldest Claude Code that runs the hooks,\n * which doctor measures the one on `PATH` against.\n */\n\nimport type { SiblingName } from './config.js';\n\nexport const MINIMUM_VERSIONS: Readonly<Record<SiblingName, string>> = {\n // The plugin `waive` hook, through which spec-brief's archive accepts a\n // protected file that a signed ruling allows.\n 'spec-brief': '0.2.0',\n // An archived brief read as a record, as init configures it, where an older\n // release reads it as retired and what depends on it as a stale premise.\n 'spec-graph': '0.9.0',\n // Scopes read with the spec-core globs the harness reads them with, and an\n // archived brief's assertions withheld rather than run.\n 'spec-guard': '0.12.0',\n};\n\nexport interface Version {\n readonly major: number;\n readonly minor: number;\n readonly patch: number;\n readonly prerelease: boolean;\n}\n\n/** A semver version, or `null`. Build metadata is accepted and ignored, as precedence ignores it. */\nexport function parseVersion(text: string): Version | null {\n const match = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/.exec(text);\n if (match === null) return null;\n return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] !== undefined };\n}\n\n/**\n * Whether `installed` is `minimum` or later by semver precedence. A minimum is\n * a release, never a prerelease, so a prerelease of the minimum comes before\n * it and a prerelease of anything later comes after it.\n */\nexport function meets(installed: Version, minimum: Version): boolean {\n const order = [installed.major - minimum.major, installed.minor - minimum.minor, installed.patch - minimum.patch].find((difference) => difference !== 0) ?? 0;\n return order > 0 || (order === 0 && !installed.prerelease);\n}\n\nexport type VersionCheck =\n | { readonly ok: true; readonly version: string }\n | { readonly ok: false; readonly version: string | null; readonly reason: string };\n\n/**\n * The `version` an installed sibling's package.json declares, against the\n * minimum. A version that cannot be read cannot be shown to meet it, and is\n * refused as an old one is.\n */\nexport function checkVersion(name: SiblingName, declared: unknown): VersionCheck {\n const minimum = MINIMUM_VERSIONS[name];\n const needs = `spec-harness needs ${minimum} or later: npm install --save-dev @descent-vtt/${name}@latest`;\n if (typeof declared !== 'string') return { ok: false, version: null, reason: `the ${name} installed here declares no version; ${needs}` };\n const parsed = parseVersion(declared);\n if (parsed === null) return { ok: false, version: declared, reason: `the ${name} installed here declares \"${declared}\", which is not a version; ${needs}` };\n if (meets(parsed, parseVersion(minimum) as Version)) return { ok: true, version: declared };\n return { ok: false, version: declared, reason: `${name} ${declared} is installed here; ${needs}` };\n}\n\n/**\n * The oldest Claude Code that runs the guard hooks (ADR-0012). Its changelog\n * adds hook `args` in 2.1.139. An older release runs the hook's bare\n * `command`, `node`, which reads the hook's input as a script and fails, and\n * a PreToolUse hook that fails with anything but exit 2 blocks nothing: every\n * write passes unguarded.\n */\nexport const CLAUDE_CODE_MINIMUM = '2.1.139';\n\n/**\n * What `claude --version` printed; the version the package.json of the\n * Claude Code a Windows shim runs declares, which is `file`; or why neither\n * could be had.\n */\nexport type ClaudeCodeAnswer = { readonly output: string } | { readonly declared: string; readonly file: string } | { readonly missing: string };\n\nexport type ClaudeCodeCheck =\n /** `file` is the package.json the version was read from, when a shim was read rather than claude run. */\n | { readonly state: 'ok' | 'outdated'; readonly version: string; readonly file?: string }\n /** Never read as fine: an older release lets every write through. */\n | { readonly state: 'unknown'; readonly reason: string };\n\n/**\n * The release `claude --version` names in its first word, `2.1.235 (Claude\n * Code)`, or the one a package.json declares, against the minimum.\n */\nexport function checkClaudeCode(answer: ClaudeCodeAnswer): ClaudeCodeCheck {\n if ('missing' in answer) return { state: 'unknown', reason: answer.missing };\n if ('declared' in answer) {\n const declared = parseVersion(answer.declared);\n if (declared === null) return { state: 'unknown', reason: `${answer.file} declares \"${answer.declared}\", which is not a version` };\n return { state: meets(declared, parseVersion(CLAUDE_CODE_MINIMUM) as Version) ? 'ok' : 'outdated', version: answer.declared, file: answer.file };\n }\n // split() gives at least one string, so the first line is always there.\n const said = answer.output.trim().split('\\n')[0]!.trim();\n if (said === '') return { state: 'unknown', reason: 'claude --version printed nothing' };\n // What was said is trimmed and not empty, so its first part ends at the\n // first space, whether the pattern takes that one or the run of them: one\n // is equivalent to its mutant.\n const first = said.split(/\\s+/)[0]!;\n const parsed = parseVersion(first);\n if (parsed === null) return { state: 'unknown', reason: `claude --version printed \"${said}\", which is not a version` };\n return { state: meets(parsed, parseVersion(CLAUDE_CODE_MINIMUM) as Version) ? 'ok' : 'outdated', version: first };\n}\n"]}
@@ -15,6 +15,15 @@ export declare const JSON_SCHEMA_VERSION = 1;
15
15
  export declare class UsageError extends Error {
16
16
  constructor(message: string);
17
17
  }
18
+ /**
19
+ * A command line the parser refused, with the command and the arguments on
20
+ * it. Whose line it was decides what the refusal is: Claude Code's hook says
21
+ * it and holds no write for it, where every other command stops (cli.ts).
22
+ */
23
+ export declare class UnreadableLine extends UsageError {
24
+ readonly words: readonly string[];
25
+ constructor(message: string, words: readonly string[]);
26
+ }
18
27
  export interface CliIO {
19
28
  readonly stdout: {
20
29
  write(text: string): unknown;
package/dist/workspace.js CHANGED
@@ -20,57 +20,73 @@ export class UsageError extends Error {
20
20
  this.name = 'UsageError';
21
21
  }
22
22
  }
23
+ /**
24
+ * A command line the parser refused, with the command and the arguments on
25
+ * it. Whose line it was decides what the refusal is: Claude Code's hook says
26
+ * it and holds no write for it, where every other command stops (cli.ts).
27
+ */
28
+ export class UnreadableLine extends UsageError {
29
+ words;
30
+ constructor(message, words) {
31
+ super(message);
32
+ this.words = words;
33
+ }
34
+ }
23
35
  export function version() {
36
+ // A Buffer parses as its text, so the encoding is equivalent to its mutant.
24
37
  const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
25
38
  return manifest.version;
26
39
  }
27
40
  export function parseOptions(argv) {
41
+ // `satisfies`, where a const assertion would read the same to the compiler:
42
+ // the sweep mutates nothing that stands under an `as`, the table with it.
43
+ const options = {
44
+ brief: { type: 'string' },
45
+ base: { type: 'string' },
46
+ root: { type: 'string' },
47
+ format: { type: 'string' },
48
+ strict: { type: 'boolean' },
49
+ write: { type: 'boolean' },
50
+ 'git-hook': { type: 'boolean' },
51
+ path: { type: 'string', multiple: true },
52
+ reason: { type: 'string' },
53
+ option: { type: 'string', multiple: true },
54
+ recommend: { type: 'string' },
55
+ list: { type: 'boolean' },
56
+ show: { type: 'string' },
57
+ allow: { type: 'boolean' },
58
+ deny: { type: 'boolean' },
59
+ note: { type: 'string' },
60
+ id: { type: 'string' },
61
+ at: { type: 'string' },
62
+ help: { type: 'boolean', short: 'h' },
63
+ version: { type: 'boolean', short: 'v' },
64
+ };
28
65
  let parsed;
29
66
  try {
30
- parsed = parseArgs({
31
- args: [...argv],
32
- allowPositionals: true,
33
- strict: true,
34
- options: {
35
- brief: { type: 'string' },
36
- base: { type: 'string' },
37
- root: { type: 'string' },
38
- format: { type: 'string' },
39
- strict: { type: 'boolean' },
40
- write: { type: 'boolean' },
41
- 'git-hook': { type: 'boolean' },
42
- path: { type: 'string', multiple: true },
43
- reason: { type: 'string' },
44
- option: { type: 'string', multiple: true },
45
- recommend: { type: 'string' },
46
- list: { type: 'boolean' },
47
- show: { type: 'string' },
48
- allow: { type: 'boolean' },
49
- deny: { type: 'boolean' },
50
- note: { type: 'string' },
51
- id: { type: 'string' },
52
- at: { type: 'string' },
53
- help: { type: 'boolean', short: 'h' },
54
- version: { type: 'boolean', short: 'v' },
55
- },
56
- });
67
+ parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options });
57
68
  }
58
69
  catch (error) {
59
- throw new UsageError(error.message);
70
+ // Read again with nothing refused, for whose line it was and nothing
71
+ // else: an option no command has is passed over, one left without its
72
+ // value takes none, and no value read this way is used. The same table,
73
+ // so that a value is not taken for the command it stands before.
74
+ throw new UnreadableLine(error.message, parseArgs({ args: [...argv], strict: false, options }).positionals);
60
75
  }
61
76
  const { values, positionals } = parsed;
62
- const format = (values.format ?? 'pretty');
77
+ const asked = values.format ?? 'pretty';
78
+ const format = asked;
63
79
  const formats = ['pretty', 'json', 'gitlab', 'sarif', 'github'];
64
80
  if (!formats.includes(format))
65
- throw new UsageError(`--format must be pretty, json, gitlab, sarif or github, not "${format}"`);
81
+ throw new UnreadableLine(`--format must be pretty, json, gitlab, sarif or github, not "${format}"`, positionals);
66
82
  const command = positionals[0];
67
83
  // Only the commands whose findings have places print them for a forge.
68
84
  if (command !== undefined && command !== 'audit' && command !== 'premises' && format !== 'pretty' && format !== 'json') {
69
- throw new UsageError(`--format ${format} is for audit and premises; ${command} prints pretty or json`);
85
+ throw new UnreadableLine(`--format ${format} is for audit and premises; ${command} prints pretty or json`, positionals);
70
86
  }
71
87
  const at = values.at;
72
88
  if (at !== undefined && at !== 'base' && at !== 'head' && at !== 'both')
73
- throw new UsageError(`--at must be base, head or both, not "${at}"`);
89
+ throw new UnreadableLine(`--at must be base, head or both, not "${at}"`, positionals);
74
90
  return {
75
91
  command,
76
92
  positionals: positionals.slice(1),
@@ -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 } 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 { loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch, 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\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 const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };\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 let parsed;\n try {\n parsed = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n 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 },\n });\n } catch (error) {\n throw new UsageError((error as Error).message);\n }\n const { values, positionals } = parsed;\n const format = (values.format ?? 'pretty') as Options['format'];\n const formats: readonly Options['format'][] = ['pretty', 'json', 'gitlab', 'sarif', 'github'];\n if (!formats.includes(format)) throw new UsageError(`--format must be pretty, json, gitlab, sarif or github, not \"${format}\"`);\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 UsageError(`--format ${format} is for audit and premises; ${command} prints pretty or json`);\n }\n const at = values.at;\n if (at !== undefined && at !== 'base' && at !== 'head' && at !== 'both') throw new UsageError(`--at must be base, head or both, not \"${at}\"`);\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: Options, io: CliIO): Promise<Workspace> {\n const cwd = options.root ?? io.cwd;\n const root = await workTreeRoot(cwd);\n if (root === null) throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);\n // Inside a work tree git always names the common directory; the fallback\n // is there for the type.\n const commonDir = (await commonDirectory(root)) ?? `${root}/.git`;\n const { config } = await loadConfig(root);\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 checkedOut = await currentBranch(root);\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/** The id the flag, the environment or the branch names, without asking spec-brief anything. */\nexport function namedId(workspace: Workspace, options: Options, 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: Options, 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 { 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 { loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch, 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: Options, io: CliIO): Promise<Workspace> {\n const cwd = options.root ?? io.cwd;\n const root = await workTreeRoot(cwd);\n if (root === null) throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);\n // Inside a work tree git always names the common directory; the fallback\n // is there for the type.\n const commonDir = (await commonDirectory(root)) ?? `${root}/.git`;\n const { config } = await loadConfig(root);\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 checkedOut = await currentBranch(root);\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/** The id the flag, the environment or the branch names, without asking spec-brief anything. */\nexport function namedId(workspace: Workspace, options: Options, 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: Options, 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descent-vtt/spec-harness",
3
- "version": "0.13.0",
3
+ "version": "0.15.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",
@@ -73,8 +73,8 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@descent-vtt/spec-brief": "^0.7.0",
77
- "@descent-vtt/spec-guard": "^0.21.0",
76
+ "@descent-vtt/spec-brief": "^0.8.0",
77
+ "@descent-vtt/spec-guard": "^0.23.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",