@descent-vtt/spec-harness 0.1.0 → 0.1.2

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/.mcp.json +5 -4
  4. package/CHANGELOG.md +69 -1
  5. package/README.md +22 -12
  6. package/dist/audit.d.ts +23 -2
  7. package/dist/audit.js +30 -1
  8. package/dist/audit.js.map +1 -1
  9. package/dist/cli.js +30 -5
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +1 -1
  12. package/dist/commands.js.map +1 -1
  13. package/dist/configure.d.ts +105 -0
  14. package/dist/configure.js +210 -0
  15. package/dist/configure.js.map +1 -0
  16. package/dist/context.d.ts +31 -4
  17. package/dist/context.js +40 -7
  18. package/dist/context.js.map +1 -1
  19. package/dist/fs.d.ts +2 -0
  20. package/dist/fs.js +12 -0
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +2 -0
  23. package/dist/git.js +5 -0
  24. package/dist/git.js.map +1 -1
  25. package/dist/guard.d.ts +2 -0
  26. package/dist/guard.js +5 -1
  27. package/dist/guard.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugin.d.ts +2 -1
  32. package/dist/plugin.js +4 -9
  33. package/dist/plugin.js.map +1 -1
  34. package/dist/premises.d.ts +4 -0
  35. package/dist/premises.js +20 -14
  36. package/dist/premises.js.map +1 -1
  37. package/dist/round.d.ts +7 -3
  38. package/dist/round.js +35 -26
  39. package/dist/round.js.map +1 -1
  40. package/dist/server.js +37 -13
  41. package/dist/server.js.map +1 -1
  42. package/dist/setup.d.ts +0 -9
  43. package/dist/setup.js +85 -68
  44. package/dist/setup.js.map +1 -1
  45. package/dist/siblings.d.ts +2 -1
  46. package/dist/siblings.js +1 -1
  47. package/dist/siblings.js.map +1 -1
  48. package/dist/versions.js +2 -2
  49. package/dist/versions.js.map +1 -1
  50. package/hooks/hooks.json +12 -2
  51. package/package.json +4 -5
  52. package/skills/close-round/SKILL.md +4 -0
package/dist/setup.js CHANGED
@@ -15,56 +15,15 @@ import { existsSync } from 'node:fs';
15
15
  import { chmod, mkdir, readFile } from 'node:fs/promises';
16
16
  import { dirname, join } from 'node:path';
17
17
  import { CONFIG_FILE } from './config.js';
18
- import { writeAtomic } from './fs.js';
19
- import { git, remoteDefault } from './git.js';
18
+ import { chooseBase, describeGraph, graphReadsBriefs, GUARD_HOOK, mergeClaudeSettings, mergeMcp, mergeSpecBrief, mergeSpecGraph, PLUGIN, SPEC_BRIEF_CONFIGS, SPEC_GRAPH_CONFIGS, } from './configure.js';
19
+ import { readJsonObject as readJson, writeAtomic } from './fs.js';
20
+ import { git, localBranches, remoteDefault } from './git.js';
20
21
  import { runSibling } from './siblings.js';
21
22
  import { EXIT_ERROR, EXIT_OK, json, openWorkspace } from './workspace.js';
22
- async function readJson(file) {
23
- if (!existsSync(file))
24
- return null;
25
- try {
26
- const value = JSON.parse(await readFile(file, 'utf8'));
27
- return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : 'unreadable';
28
- }
29
- catch {
30
- return 'unreadable';
31
- }
32
- }
33
23
  function stringify(value) {
34
24
  return `${JSON.stringify(value, null, 2)}\n`;
35
25
  }
36
- export const HOOK_COMMAND = 'npx --no-install spec-harness hook claude';
37
- const HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
38
- /** `.claude/settings.json` with the guard hooks merged in, or `null` when they are already there. */
39
- export function mergeClaudeSettings(current) {
40
- const hooks = (typeof current['hooks'] === 'object' && current['hooks'] !== null ? current['hooks'] : {});
41
- let changed = false;
42
- const next = { ...hooks };
43
- for (const event of ['PreToolUse', 'PostToolUse']) {
44
- const groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
45
- const present = groups.some((group) => Array.isArray(group['hooks']) && group['hooks'].some((hook) => hook['command'] === HOOK_COMMAND));
46
- if (present)
47
- continue;
48
- groups.push({ matcher: HOOK_MATCHER, hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 60 }] });
49
- next[event] = groups;
50
- changed = true;
51
- }
52
- return changed ? { ...current, hooks: next } : null;
53
- }
54
- /** `.mcp.json` with the harness's server merged in, or `null` when it is already there. */
55
- export function mergeMcp(current) {
56
- const servers = (typeof current['mcpServers'] === 'object' && current['mcpServers'] !== null ? current['mcpServers'] : {});
57
- if ('spec-harness' in servers)
58
- return null;
59
- return { ...current, mcpServers: { ...servers, 'spec-harness': { command: 'npx', args: ['--no-install', 'spec-harness', 'mcp'] } } };
60
- }
61
- /** spec-graph's configuration with the archive read as history, or `null` when it already is. */
62
- export function mergeSpecGraph(current, archiveGlob) {
63
- const history = Array.isArray(current['historyPatterns']) ? current['historyPatterns'].filter((p) => typeof p === 'string') : [];
64
- if (history.includes(archiveGlob))
65
- return null;
66
- return { ...current, historyPatterns: [...history, archiveGlob] };
67
- }
26
+ const PLUGIN_DETAIL = `load spec-harness's plugin, "${PLUGIN}": spec-brief's archive asks it whether a signed ruling allows a protected file, and refuses the file without it`;
68
27
  const PRE_COMMIT = `#!/bin/sh
69
28
  # spec-harness: refuse a commit that changes what the active brief protects.
70
29
  exec npx --no-install spec-harness hook git
@@ -73,13 +32,14 @@ export async function plan(workspace, options) {
73
32
  const { root } = workspace;
74
33
  const steps = [];
75
34
  // spec-brief first: every other setting is derived from its directories.
76
- const briefConfigFile = ['.spec-brief.json', 'spec-brief.json'].map((name) => join(root, name)).find((file) => existsSync(file));
35
+ const briefConfig = SPEC_BRIEF_CONFIGS.find((name) => existsSync(join(root, name)));
77
36
  const brief = workspace.siblings.locate('spec-brief');
78
37
  let briefs = 'briefs';
79
38
  let archive = 'briefs/archive';
80
- if (briefConfigFile === undefined) {
39
+ if (briefConfig === undefined) {
81
40
  if (brief.kind === 'found') {
82
41
  const command = brief.command;
42
+ const created = join(root, '.spec-brief.json');
83
43
  steps.push({
84
44
  file: '.spec-brief.json',
85
45
  action: 'run',
@@ -90,61 +50,113 @@ export async function plan(workspace, options) {
90
50
  throw new Error(`spec-brief init failed: ${run.stderr.trim()}`);
91
51
  },
92
52
  });
53
+ steps.push({
54
+ file: '.spec-brief.json',
55
+ action: 'update',
56
+ detail: PLUGIN_DETAIL,
57
+ apply: async () => {
58
+ const written = await readJson(created);
59
+ if (written === null || written === 'unreadable')
60
+ throw new Error('spec-brief init wrote no configuration JSON can read');
61
+ const merged = mergeSpecBrief(written);
62
+ if (merged !== null)
63
+ await writeAtomic(created, stringify(merged));
64
+ },
65
+ });
93
66
  }
94
67
  else {
95
68
  steps.push({ file: '.spec-brief.json', action: 'advise', detail: brief.reason });
96
69
  }
97
70
  }
98
71
  else {
99
- const config = await readJson(briefConfigFile);
72
+ const file = join(root, briefConfig);
73
+ const config = await readJson(file);
100
74
  if (config !== null && config !== 'unreadable') {
101
75
  if (typeof config['briefs'] === 'string')
102
76
  briefs = config['briefs'];
103
77
  archive = typeof config['archive'] === 'string' ? config['archive'] : `${briefs}/archive`;
104
78
  }
105
- steps.push({ file: briefConfigFile.slice(root.length + 1), action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });
79
+ steps.push({ file: briefConfig, action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });
80
+ // spec-brief's archive learns that a signed ruling allows a protected file
81
+ // only from this package's plugin (ADR-0006).
82
+ if (config === 'unreadable' || config === null) {
83
+ steps.push({ file: briefConfig, action: 'advise', detail: `cannot be read as JSON; add "plugins": ["${PLUGIN}"] by hand` });
84
+ }
85
+ else {
86
+ const merged = mergeSpecBrief(config);
87
+ if (merged === null)
88
+ steps.push({ file: briefConfig, action: 'keep', detail: "spec-harness's plugin is loaded" });
89
+ else
90
+ steps.push({ file: briefConfig, action: 'update', detail: PLUGIN_DETAIL, apply: () => writeAtomic(file, stringify(merged)) });
91
+ }
106
92
  }
107
- // spec-graph: an archived brief is a record, not a retired decision.
108
- const graphFile = join(root, '.spec-graph.json');
93
+ // spec-graph: an archived brief is a record, not a retired decision. Its
94
+ // configuration is the first of its files, then a key in package.json; a
95
+ // file written beside that key would shadow it.
109
96
  const graph = workspace.siblings.locate('spec-graph');
110
97
  if (graph.kind === 'outdated')
111
98
  steps.push({ file: '.spec-graph.json', action: 'advise', detail: graph.reason });
112
99
  if (graph.kind === 'found') {
113
- const current = await readJson(graphFile);
114
100
  const archiveGlob = `${archive}/**`;
115
- if (current === 'unreadable') {
116
- steps.push({ file: '.spec-graph.json', action: 'advise', detail: 'cannot be read as JSON; add "historyPatterns": ["' + archiveGlob + '"] by hand' });
101
+ const graphName = SPEC_GRAPH_CONFIGS.find((name) => existsSync(join(root, name)));
102
+ const manifest = graphName === undefined ? await readJson(join(root, 'package.json')) : null;
103
+ if (manifest !== null && manifest !== 'unreadable' && 'spec-graph' in manifest) {
104
+ steps.push({ file: 'package.json', action: 'advise', detail: `spec-graph reads its configuration from the "spec-graph" key here, which init does not edit: add "${archiveGlob}" to its "historyPatterns" by hand` });
117
105
  }
118
106
  else {
119
- const merged = mergeSpecGraph(current ?? {}, archiveGlob);
120
- if (merged === null)
121
- steps.push({ file: '.spec-graph.json', action: 'keep', detail: `${archiveGlob} is already history` });
122
- else
123
- steps.push({ file: '.spec-graph.json', action: current === null ? 'create' : 'update', detail: `read ${archiveGlob} as history, so a brief that depends on an archived one is not a stale premise`, apply: () => writeAtomic(graphFile, stringify(merged)) });
107
+ const file = graphName ?? '.spec-graph.json';
108
+ const current = await readJson(join(root, file));
109
+ if (current === 'unreadable') {
110
+ steps.push({ file, action: 'advise', detail: `cannot be read as JSON; add "historyPatterns": ["${archiveGlob}"] by hand` });
111
+ }
112
+ else {
113
+ const merged = mergeSpecGraph(current ?? {}, archiveGlob);
114
+ const detail = describeGraph(archiveGlob, briefs, graphReadsBriefs(current ?? {}, briefs), merged !== null);
115
+ if (merged === null)
116
+ steps.push({ file, action: 'keep', detail });
117
+ else
118
+ steps.push({ file, action: current === null ? 'create' : 'update', detail, apply: () => writeAtomic(join(root, file), stringify(merged)) });
119
+ }
124
120
  }
125
121
  }
126
- // spec-harness's own file, naming the base the rounds merge into.
122
+ // spec-harness's own file, naming the base the rounds merge into: every
123
+ // ruling is verified against the allowed signers on it.
127
124
  const harnessFile = join(root, CONFIG_FILE);
128
- if (!existsSync(harnessFile)) {
129
- const base = await remoteDefault(root);
130
- const content = base === null ? {} : { base };
131
- steps.push({ file: CONFIG_FILE, action: 'create', detail: base === null ? 'the defaults' : `rounds are measured from ${base}`, apply: () => writeAtomic(harnessFile, stringify(content)) });
125
+ const harness = await readJson(harnessFile);
126
+ if (harness === 'unreadable') {
127
+ steps.push({ file: CONFIG_FILE, action: 'advise', detail: 'cannot be read as JSON; name the base rounds merge into as "base" by hand' });
128
+ }
129
+ else if (harness !== null && typeof harness['base'] === 'string') {
130
+ steps.push({ file: CONFIG_FILE, action: 'keep', detail: `rounds are measured from ${harness['base']}` });
132
131
  }
133
132
  else {
134
- steps.push({ file: CONFIG_FILE, action: 'keep', detail: 'already configured' });
133
+ const choice = chooseBase({ remoteDefault: await remoteDefault(root), branch: workspace.branch, branches: await localBranches(root) });
134
+ const base = choice.base;
135
+ if (base === null) {
136
+ steps.push({ file: CONFIG_FILE, action: 'advise', detail: choice.detail });
137
+ }
138
+ else {
139
+ const content = { ...harness, base };
140
+ steps.push({ file: CONFIG_FILE, action: harness === null ? 'create' : 'update', detail: choice.detail, apply: () => writeAtomic(harnessFile, stringify(content)) });
141
+ }
135
142
  }
136
143
  // The agent's hook and server.
137
144
  const settingsFile = join(root, '.claude', 'settings.json');
138
145
  const settings = await readJson(settingsFile);
139
146
  if (settings === 'unreadable') {
140
- steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add a PreToolUse and a PostToolUse hook running "${HOOK_COMMAND}" by hand` });
147
+ steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });
141
148
  }
142
149
  else {
143
150
  const merged = mergeClaudeSettings(settings ?? {});
144
151
  if (merged === null)
145
152
  steps.push({ file: '.claude/settings.json', action: 'keep', detail: 'the guard hooks are installed' });
146
153
  else
147
- steps.push({ file: '.claude/settings.json', action: settings === null ? 'create' : 'update', detail: 'a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope', apply: () => writeAtomic(settingsFile, stringify(merged)) });
154
+ steps.push({
155
+ file: '.claude/settings.json',
156
+ action: settings === null ? 'create' : 'update',
157
+ detail: 'the guard hooks, run with node from the project\'s install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope',
158
+ apply: () => writeAtomic(settingsFile, stringify(merged)),
159
+ });
148
160
  }
149
161
  const mcpFile = join(root, '.mcp.json');
150
162
  const mcp = await readJson(mcpFile);
@@ -156,7 +168,12 @@ export async function plan(workspace, options) {
156
168
  if (merged === null)
157
169
  steps.push({ file: '.mcp.json', action: 'keep', detail: 'the spec-harness server is registered' });
158
170
  else
159
- steps.push({ file: '.mcp.json', action: mcp === null ? 'create' : 'update', detail: 'the spec-harness MCP server: start_round, check_path, request_escalation, audit_round, list_rounds', apply: () => writeAtomic(mcpFile, stringify(merged)) });
171
+ steps.push({
172
+ file: '.mcp.json',
173
+ action: mcp === null ? 'create' : 'update',
174
+ detail: 'the spec-harness MCP server, run with node from the project\'s install: start_round, check_path, request_escalation, audit_round, list_rounds',
175
+ apply: () => writeAtomic(mcpFile, stringify(merged)),
176
+ });
160
177
  }
161
178
  // git's hook, only when asked: it is configuration outside the tree.
162
179
  if (options.gitHook) {
package/dist/setup.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness init`: the family configured to agree, from the first day.\n *\n * The integration faults found when the tools were first run together were all\n * configuration: spec-graph read an archived brief as a retired decision and\n * reported every brief that depended on one; spec-guard executed an archived\n * brief's assertions. Each tool can be told the truth; `init` tells each of\n * them the same truth, derived from spec-brief's own settings.\n *\n * It prints a plan and changes nothing until `--write`. It merges into files\n * that exist and never replaces what a person wrote; a git hook the\n * repository already has is left alone, with the line to add printed instead.\n */\n\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nimport { CONFIG_FILE } from './config.js';\nimport { writeAtomic } from './fs.js';\nimport { git, remoteDefault } from './git.js';\nimport { runSibling } from './siblings.js';\nimport { EXIT_ERROR, EXIT_OK, json, openWorkspace, type CliIO, type Options, type Workspace } from './workspace.js';\n\nexport type Action = 'create' | 'update' | 'keep' | 'run' | 'advise';\n\nexport interface Step {\n readonly file: string;\n readonly action: Action;\n readonly detail: string;\n apply?: () => Promise<void>;\n}\n\ntype Json = Record<string, unknown>;\n\nasync function readJson(file: string): Promise<Json | null | 'unreadable'> {\n if (!existsSync(file)) return null;\n try {\n const value = JSON.parse(await readFile(file, 'utf8')) as unknown;\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Json) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nexport const HOOK_COMMAND = 'npx --no-install spec-harness hook claude';\nconst HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';\n\n/** `.claude/settings.json` with the guard hooks merged in, or `null` when they are already there. */\nexport function mergeClaudeSettings(current: Json): Json | null {\n const hooks = (typeof current['hooks'] === 'object' && current['hooks'] !== null ? current['hooks'] : {}) as Json;\n let changed = false;\n const next: Json = { ...hooks };\n for (const event of ['PreToolUse', 'PostToolUse']) {\n const groups = Array.isArray(hooks[event]) ? [...(hooks[event] as Json[])] : [];\n const present = groups.some((group) => Array.isArray(group['hooks']) && (group['hooks'] as Json[]).some((hook) => hook['command'] === HOOK_COMMAND));\n if (present) continue;\n groups.push({ matcher: HOOK_MATCHER, hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 60 }] });\n next[event] = groups;\n changed = true;\n }\n return changed ? { ...current, hooks: next } : null;\n}\n\n/** `.mcp.json` with the harness's server merged in, or `null` when it is already there. */\nexport function mergeMcp(current: Json): Json | null {\n const servers = (typeof current['mcpServers'] === 'object' && current['mcpServers'] !== null ? current['mcpServers'] : {}) as Json;\n if ('spec-harness' in servers) return null;\n return { ...current, mcpServers: { ...servers, 'spec-harness': { command: 'npx', args: ['--no-install', 'spec-harness', 'mcp'] } } };\n}\n\n/** spec-graph's configuration with the archive read as history, or `null` when it already is. */\nexport function mergeSpecGraph(current: Json, archiveGlob: string): Json | null {\n const history = Array.isArray(current['historyPatterns']) ? (current['historyPatterns'] as unknown[]).filter((p): p is string => typeof p === 'string') : [];\n if (history.includes(archiveGlob)) return null;\n return { ...current, historyPatterns: [...history, archiveGlob] };\n}\n\nconst PRE_COMMIT = `#!/bin/sh\n# spec-harness: refuse a commit that changes what the active brief protects.\nexec npx --no-install spec-harness hook git\n`;\n\nexport async function plan(workspace: Workspace, options: Options): Promise<Step[]> {\n const { root } = workspace;\n const steps: Step[] = [];\n\n // spec-brief first: every other setting is derived from its directories.\n const briefConfigFile = ['.spec-brief.json', 'spec-brief.json'].map((name) => join(root, name)).find((file) => existsSync(file));\n const brief = workspace.siblings.locate('spec-brief');\n let briefs = 'briefs';\n let archive = 'briefs/archive';\n if (briefConfigFile === undefined) {\n if (brief.kind === 'found') {\n const command = brief.command;\n steps.push({\n file: '.spec-brief.json',\n action: 'run',\n detail: 'spec-brief init: the brief and archive directories, every default spelled out',\n apply: async () => {\n const run = await runSibling(command, ['init'], root);\n if (run.code !== 0) throw new Error(`spec-brief init failed: ${run.stderr.trim()}`);\n },\n });\n } else {\n steps.push({ file: '.spec-brief.json', action: 'advise', detail: brief.reason });\n }\n } else {\n const config = await readJson(briefConfigFile);\n if (config !== null && config !== 'unreadable') {\n if (typeof config['briefs'] === 'string') briefs = config['briefs'];\n archive = typeof config['archive'] === 'string' ? config['archive'] : `${briefs}/archive`;\n }\n steps.push({ file: briefConfigFile.slice(root.length + 1), action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });\n }\n\n // spec-graph: an archived brief is a record, not a retired decision.\n const graphFile = join(root, '.spec-graph.json');\n const graph = workspace.siblings.locate('spec-graph');\n if (graph.kind === 'outdated') steps.push({ file: '.spec-graph.json', action: 'advise', detail: graph.reason });\n if (graph.kind === 'found') {\n const current = await readJson(graphFile);\n const archiveGlob = `${archive}/**`;\n if (current === 'unreadable') {\n steps.push({ file: '.spec-graph.json', action: 'advise', detail: 'cannot be read as JSON; add \"historyPatterns\": [\"' + archiveGlob + '\"] by hand' });\n } else {\n const merged = mergeSpecGraph(current ?? {}, archiveGlob);\n if (merged === null) steps.push({ file: '.spec-graph.json', action: 'keep', detail: `${archiveGlob} is already history` });\n else steps.push({ file: '.spec-graph.json', action: current === null ? 'create' : 'update', detail: `read ${archiveGlob} as history, so a brief that depends on an archived one is not a stale premise`, apply: () => writeAtomic(graphFile, stringify(merged)) });\n }\n }\n\n // spec-harness's own file, naming the base the rounds merge into.\n const harnessFile = join(root, CONFIG_FILE);\n if (!existsSync(harnessFile)) {\n const base = await remoteDefault(root);\n const content: Json = base === null ? {} : { base };\n steps.push({ file: CONFIG_FILE, action: 'create', detail: base === null ? 'the defaults' : `rounds are measured from ${base}`, apply: () => writeAtomic(harnessFile, stringify(content)) });\n } else {\n steps.push({ file: CONFIG_FILE, action: 'keep', detail: 'already configured' });\n }\n\n // The agent's hook and server.\n const settingsFile = join(root, '.claude', 'settings.json');\n const settings = await readJson(settingsFile);\n if (settings === 'unreadable') {\n steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add a PreToolUse and a PostToolUse hook running \"${HOOK_COMMAND}\" by hand` });\n } else {\n const merged = mergeClaudeSettings(settings ?? {});\n if (merged === null) steps.push({ file: '.claude/settings.json', action: 'keep', detail: 'the guard hooks are installed' });\n else steps.push({ file: '.claude/settings.json', action: settings === null ? 'create' : 'update', detail: 'a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope', apply: () => writeAtomic(settingsFile, stringify(merged)) });\n }\n const mcpFile = join(root, '.mcp.json');\n const mcp = await readJson(mcpFile);\n if (mcp === 'unreadable') {\n steps.push({ file: '.mcp.json', action: 'advise', detail: 'cannot be read as JSON; add the spec-harness server by hand' });\n } else {\n const merged = mergeMcp(mcp ?? {});\n if (merged === null) steps.push({ file: '.mcp.json', action: 'keep', detail: 'the spec-harness server is registered' });\n else steps.push({ file: '.mcp.json', action: mcp === null ? 'create' : 'update', detail: 'the spec-harness MCP server: start_round, check_path, request_escalation, audit_round, list_rounds', apply: () => writeAtomic(mcpFile, stringify(merged)) });\n }\n\n // git's hook, only when asked: it is configuration outside the tree.\n if (options.gitHook) {\n const hooksPath = (await git(['config', '--get', 'core.hooksPath'], root)).stdout.trim();\n const hookFile = hooksPath === '' ? join(workspace.commonDir, 'hooks', 'pre-commit') : join(root, hooksPath, 'pre-commit');\n if (existsSync(hookFile)) {\n const text = await readFile(hookFile, 'utf8');\n steps.push(\n text.includes('spec-harness')\n ? { file: hookFile, action: 'keep', detail: 'already runs spec-harness' }\n : { file: hookFile, action: 'advise', detail: 'a pre-commit hook exists; add the line \"npx --no-install spec-harness hook git\" to it' },\n );\n } else {\n steps.push({\n file: hookFile,\n action: 'create',\n detail: 'refuse a commit that changes what the active brief protects',\n apply: async () => {\n await mkdir(dirname(hookFile), { recursive: true });\n await writeAtomic(hookFile, PRE_COMMIT);\n await chmod(hookFile, 0o755);\n },\n });\n }\n }\n\n steps.push({\n file: '.github/allowed_signers',\n action: 'advise',\n detail:\n 'rulings count only when signed by a key listed here on the base branch: one line per person, \"<email> namespaces=\\\\\"git\\\\\" <public key>\"; protect it, the ADRs and the tool configurations with CODEOWNERS',\n });\n return steps;\n}\n\nexport async function initCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const steps = await plan(workspace, options);\n if (options.write) {\n for (const step of steps) {\n if (step.apply !== undefined) {\n try {\n await step.apply();\n } catch (error) {\n io.stderr.write(`spec-harness: ${step.file}: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n }\n }\n if (options.format === 'json') {\n io.stdout.write(json('init', { written: options.write, steps: steps.map(({ file, action, detail }) => ({ file, action, detail })) }));\n } else {\n for (const step of steps) io.stdout.write(`${step.action.padEnd(6)} ${step.file}\\n ${step.detail}\\n`);\n if (!options.write && steps.some((step) => step.apply !== undefined)) io.stdout.write('\\nNothing was changed. Run again with --write to apply the plan.\\n');\n }\n return EXIT_OK;\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness init`: the family configured to agree, from the first day.\n *\n * The integration faults found when the tools were first run together were all\n * configuration: spec-graph read an archived brief as a retired decision and\n * reported every brief that depended on one; spec-guard executed an archived\n * brief's assertions. Each tool can be told the truth; `init` tells each of\n * them the same truth, derived from spec-brief's own settings.\n *\n * It prints a plan and changes nothing until `--write`. It merges into files\n * that exist and never replaces what a person wrote; a git hook the\n * repository already has is left alone, with the line to add printed instead.\n */\n\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nimport { CONFIG_FILE } from './config.js';\nimport {\n chooseBase,\n describeGraph,\n graphReadsBriefs,\n GUARD_HOOK,\n mergeClaudeSettings,\n mergeMcp,\n mergeSpecBrief,\n mergeSpecGraph,\n PLUGIN,\n SPEC_BRIEF_CONFIGS,\n SPEC_GRAPH_CONFIGS,\n} from './configure.js';\nimport { readJsonObject as readJson, writeAtomic } from './fs.js';\nimport { git, localBranches, remoteDefault } from './git.js';\nimport { runSibling } from './siblings.js';\nimport { EXIT_ERROR, EXIT_OK, json, openWorkspace, type CliIO, type Options, type Workspace } from './workspace.js';\n\nexport type Action = 'create' | 'update' | 'keep' | 'run' | 'advise';\n\nexport interface Step {\n readonly file: string;\n readonly action: Action;\n readonly detail: string;\n apply?: () => Promise<void>;\n}\n\ntype Json = Record<string, unknown>;\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nconst PLUGIN_DETAIL = `load spec-harness's plugin, \"${PLUGIN}\": spec-brief's archive asks it whether a signed ruling allows a protected file, and refuses the file without it`;\n\nconst PRE_COMMIT = `#!/bin/sh\n# spec-harness: refuse a commit that changes what the active brief protects.\nexec npx --no-install spec-harness hook git\n`;\n\nexport async function plan(workspace: Workspace, options: Options): Promise<Step[]> {\n const { root } = workspace;\n const steps: Step[] = [];\n\n // spec-brief first: every other setting is derived from its directories.\n const briefConfig = SPEC_BRIEF_CONFIGS.find((name) => existsSync(join(root, name)));\n const brief = workspace.siblings.locate('spec-brief');\n let briefs = 'briefs';\n let archive = 'briefs/archive';\n if (briefConfig === undefined) {\n if (brief.kind === 'found') {\n const command = brief.command;\n const created = join(root, '.spec-brief.json');\n steps.push({\n file: '.spec-brief.json',\n action: 'run',\n detail: 'spec-brief init: the brief and archive directories, every default spelled out',\n apply: async () => {\n const run = await runSibling(command, ['init'], root);\n if (run.code !== 0) throw new Error(`spec-brief init failed: ${run.stderr.trim()}`);\n },\n });\n steps.push({\n file: '.spec-brief.json',\n action: 'update',\n detail: PLUGIN_DETAIL,\n apply: async () => {\n const written = await readJson(created);\n if (written === null || written === 'unreadable') throw new Error('spec-brief init wrote no configuration JSON can read');\n const merged = mergeSpecBrief(written);\n if (merged !== null) await writeAtomic(created, stringify(merged));\n },\n });\n } else {\n steps.push({ file: '.spec-brief.json', action: 'advise', detail: brief.reason });\n }\n } else {\n const file = join(root, briefConfig);\n const config = await readJson(file);\n if (config !== null && config !== 'unreadable') {\n if (typeof config['briefs'] === 'string') briefs = config['briefs'];\n archive = typeof config['archive'] === 'string' ? config['archive'] : `${briefs}/archive`;\n }\n steps.push({ file: briefConfig, action: 'keep', detail: `briefs in ${briefs}/, the archive in ${archive}/` });\n // spec-brief's archive learns that a signed ruling allows a protected file\n // only from this package's plugin (ADR-0006).\n if (config === 'unreadable' || config === null) {\n steps.push({ file: briefConfig, action: 'advise', detail: `cannot be read as JSON; add \"plugins\": [\"${PLUGIN}\"] by hand` });\n } else {\n const merged = mergeSpecBrief(config);\n if (merged === null) steps.push({ file: briefConfig, action: 'keep', detail: \"spec-harness's plugin is loaded\" });\n else steps.push({ file: briefConfig, action: 'update', detail: PLUGIN_DETAIL, apply: () => writeAtomic(file, stringify(merged)) });\n }\n }\n\n // spec-graph: an archived brief is a record, not a retired decision. Its\n // configuration is the first of its files, then a key in package.json; a\n // file written beside that key would shadow it.\n const graph = workspace.siblings.locate('spec-graph');\n if (graph.kind === 'outdated') steps.push({ file: '.spec-graph.json', action: 'advise', detail: graph.reason });\n if (graph.kind === 'found') {\n const archiveGlob = `${archive}/**`;\n const graphName = SPEC_GRAPH_CONFIGS.find((name) => existsSync(join(root, name)));\n const manifest = graphName === undefined ? await readJson(join(root, 'package.json')) : null;\n if (manifest !== null && manifest !== 'unreadable' && 'spec-graph' in manifest) {\n steps.push({ file: 'package.json', action: 'advise', detail: `spec-graph reads its configuration from the \"spec-graph\" key here, which init does not edit: add \"${archiveGlob}\" to its \"historyPatterns\" by hand` });\n } else {\n const file = graphName ?? '.spec-graph.json';\n const current = await readJson(join(root, file));\n if (current === 'unreadable') {\n steps.push({ file, action: 'advise', detail: `cannot be read as JSON; add \"historyPatterns\": [\"${archiveGlob}\"] by hand` });\n } else {\n const merged = mergeSpecGraph(current ?? {}, archiveGlob);\n const detail = describeGraph(archiveGlob, briefs, graphReadsBriefs(current ?? {}, briefs), merged !== null);\n if (merged === null) steps.push({ file, action: 'keep', detail });\n else steps.push({ file, action: current === null ? 'create' : 'update', detail, apply: () => writeAtomic(join(root, file), stringify(merged)) });\n }\n }\n }\n\n // spec-harness's own file, naming the base the rounds merge into: every\n // ruling is verified against the allowed signers on it.\n const harnessFile = join(root, CONFIG_FILE);\n const harness = await readJson(harnessFile);\n if (harness === 'unreadable') {\n steps.push({ file: CONFIG_FILE, action: 'advise', detail: 'cannot be read as JSON; name the base rounds merge into as \"base\" by hand' });\n } else if (harness !== null && typeof harness['base'] === 'string') {\n steps.push({ file: CONFIG_FILE, action: 'keep', detail: `rounds are measured from ${harness['base']}` });\n } else {\n const choice = chooseBase({ remoteDefault: await remoteDefault(root), branch: workspace.branch, branches: await localBranches(root) });\n const base = choice.base;\n if (base === null) {\n steps.push({ file: CONFIG_FILE, action: 'advise', detail: choice.detail });\n } else {\n const content: Json = { ...harness, base };\n steps.push({ file: CONFIG_FILE, action: harness === null ? 'create' : 'update', detail: choice.detail, apply: () => writeAtomic(harnessFile, stringify(content)) });\n }\n }\n\n // The agent's hook and server.\n const settingsFile = join(root, '.claude', 'settings.json');\n const settings = await readJson(settingsFile);\n if (settings === 'unreadable') {\n steps.push({ file: '.claude/settings.json', action: 'advise', detail: `cannot be read as JSON; add the guard as a PreToolUse and a PostToolUse hook by hand: ${JSON.stringify(GUARD_HOOK)}` });\n } else {\n const merged = mergeClaudeSettings(settings ?? {});\n if (merged === null) steps.push({ file: '.claude/settings.json', action: 'keep', detail: 'the guard hooks are installed' });\n else\n steps.push({\n file: '.claude/settings.json',\n action: settings === null ? 'create' : 'update',\n detail: 'the guard hooks, run with node from the project\\'s install: a PreToolUse hook refuses writes to protected files; a PostToolUse hook warns about writes outside the scope',\n apply: () => writeAtomic(settingsFile, stringify(merged)),\n });\n }\n const mcpFile = join(root, '.mcp.json');\n const mcp = await readJson(mcpFile);\n if (mcp === 'unreadable') {\n steps.push({ file: '.mcp.json', action: 'advise', detail: 'cannot be read as JSON; add the spec-harness server by hand' });\n } else {\n const merged = mergeMcp(mcp ?? {});\n if (merged === null) steps.push({ file: '.mcp.json', action: 'keep', detail: 'the spec-harness server is registered' });\n else\n steps.push({\n file: '.mcp.json',\n action: mcp === null ? 'create' : 'update',\n detail: 'the spec-harness MCP server, run with node from the project\\'s install: start_round, check_path, request_escalation, audit_round, list_rounds',\n apply: () => writeAtomic(mcpFile, stringify(merged)),\n });\n }\n\n // git's hook, only when asked: it is configuration outside the tree.\n if (options.gitHook) {\n const hooksPath = (await git(['config', '--get', 'core.hooksPath'], root)).stdout.trim();\n const hookFile = hooksPath === '' ? join(workspace.commonDir, 'hooks', 'pre-commit') : join(root, hooksPath, 'pre-commit');\n if (existsSync(hookFile)) {\n const text = await readFile(hookFile, 'utf8');\n steps.push(\n text.includes('spec-harness')\n ? { file: hookFile, action: 'keep', detail: 'already runs spec-harness' }\n : { file: hookFile, action: 'advise', detail: 'a pre-commit hook exists; add the line \"npx --no-install spec-harness hook git\" to it' },\n );\n } else {\n steps.push({\n file: hookFile,\n action: 'create',\n detail: 'refuse a commit that changes what the active brief protects',\n apply: async () => {\n await mkdir(dirname(hookFile), { recursive: true });\n await writeAtomic(hookFile, PRE_COMMIT);\n await chmod(hookFile, 0o755);\n },\n });\n }\n }\n\n steps.push({\n file: '.github/allowed_signers',\n action: 'advise',\n detail:\n 'rulings count only when signed by a key listed here on the base branch: one line per person, \"<email> namespaces=\\\\\"git\\\\\" <public key>\"; protect it, the ADRs and the tool configurations with CODEOWNERS',\n });\n return steps;\n}\n\nexport async function initCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const steps = await plan(workspace, options);\n if (options.write) {\n for (const step of steps) {\n if (step.apply !== undefined) {\n try {\n await step.apply();\n } catch (error) {\n io.stderr.write(`spec-harness: ${step.file}: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n }\n }\n }\n if (options.format === 'json') {\n io.stdout.write(json('init', { written: options.write, steps: steps.map(({ file, action, detail }) => ({ file, action, detail })) }));\n } else {\n for (const step of steps) io.stdout.write(`${step.action.padEnd(6)} ${step.file}\\n ${step.detail}\\n`);\n if (!options.write && steps.some((step) => step.apply !== undefined)) io.stdout.write('\\nNothing was changed. Run again with --write to apply the plan.\\n');\n }\n return EXIT_OK;\n}\n"]}
@@ -55,10 +55,11 @@ export interface Siblings {
55
55
  locate(name: SiblingName): Sibling;
56
56
  /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */
57
57
  briefs(): Promise<BriefRow[]>;
58
- /** A sibling's JSON output for a command line, with its exit code; `absent` says why it cannot be run, missing or outdated. */
58
+ /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */
59
59
  json(name: SiblingName, args: readonly string[]): Promise<{
60
60
  readonly code: number;
61
61
  readonly document: unknown;
62
+ readonly stderr: string;
62
63
  } | {
63
64
  readonly absent: string;
64
65
  }>;
package/dist/siblings.js CHANGED
@@ -127,7 +127,7 @@ export function createSiblings(root, config) {
127
127
  if (sibling.kind !== 'found')
128
128
  return { absent: sibling.reason };
129
129
  const run = await runSibling(sibling.command, args, root);
130
- return { code: run.code, document: parseJson(name, run) };
130
+ return { code: run.code, document: parseJson(name, run), stderr: run.stderr };
131
131
  },
132
132
  };
133
133
  }
@@ -1 +1 @@
1
- {"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /** `version` is `null` for a command `.spec-harness.json` names, whose version is not checked. */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null };\n\n/** Where a sibling is, or why it cannot be run. */\nexport function locate(name: SiblingName, root: string, config: HarnessConfig): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n const directory = join(root, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) {\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n }\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version };\n return { kind: 'found', command: [process.execPath, bin], version: check.version };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nfunction declaredVersion(file: string): unknown {\n try {\n const manifest: unknown = JSON.parse(readFileSync(file, 'utf8'));\n return typeof manifest === 'object' && manifest !== null ? (manifest as { version?: unknown }).version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n */\nexport function runSibling(command: readonly string[], args: readonly string[], root: string, timeoutMs = 600_000): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n windowsHide: true,\n timeout: timeoutMs,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */\n briefs(): Promise<BriefRow[]>;\n /** A sibling's JSON output for a command line, with its exit code; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown } | { readonly absent: string }>;\n}\n\nexport function createSiblings(root: string, config: HarnessConfig): Siblings {\n const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n return sibling.command;\n };\n return {\n root,\n locate: (name) => locate(name, root, config),\n async briefs() {\n const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] ?? 'exit 2'}`);\n }\n try {\n return parseBriefList(parseJson('spec-brief', run));\n } catch (error) {\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n },\n async json(name, args) {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run) };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /** `version` is `null` for a command `.spec-harness.json` names, whose version is not checked. */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null };\n\n/** Where a sibling is, or why it cannot be run. */\nexport function locate(name: SiblingName, root: string, config: HarnessConfig): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n const directory = join(root, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) {\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n }\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version };\n return { kind: 'found', command: [process.execPath, bin], version: check.version };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nfunction declaredVersion(file: string): unknown {\n try {\n const manifest: unknown = JSON.parse(readFileSync(file, 'utf8'));\n return typeof manifest === 'object' && manifest !== null ? (manifest as { version?: unknown }).version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n */\nexport function runSibling(command: readonly string[], args: readonly string[], root: string, timeoutMs = 600_000): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n windowsHide: true,\n timeout: timeoutMs,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */\n briefs(): Promise<BriefRow[]>;\n /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown; readonly stderr: string } | { readonly absent: string }>;\n}\n\nexport function createSiblings(root: string, config: HarnessConfig): Siblings {\n const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n return sibling.command;\n };\n return {\n root,\n locate: (name) => locate(name, root, config),\n async briefs() {\n const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] ?? 'exit 2'}`);\n }\n try {\n return parseBriefList(parseJson('spec-brief', run));\n } catch (error) {\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n },\n async json(name, args) {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run), stderr: run.stderr };\n },\n };\n}\n"]}
package/dist/versions.js CHANGED
@@ -34,8 +34,8 @@ export function parseVersion(text) {
34
34
  * it and a prerelease of anything later comes after it.
35
35
  */
36
36
  export function meets(installed, minimum) {
37
- const order = [installed.major - minimum.major, installed.minor - minimum.minor, installed.patch - minimum.patch].find((difference) => difference !== 0);
38
- return order === undefined ? !installed.prerelease : order > 0;
37
+ const order = [installed.major - minimum.major, installed.minor - minimum.minor, installed.patch - minimum.patch].find((difference) => difference !== 0) ?? 0;
38
+ return order > 0 || (order === 0 && !installed.prerelease);
39
39
  }
40
40
  /**
41
41
  * The `version` an installed sibling's package.json declares, against the
@@ -1 +1 @@
1
- {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAA0C;IACrE,wEAAwE;IACxE,8CAA8C;IAC9C,YAAY,EAAE,OAAO;IACrB,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY,EAAE,OAAO;IACrB,2EAA2E;IAC3E,wDAAwD;IACxD,YAAY,EAAE,QAAQ;CACvB,CAAC;AASF,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,yHAAyH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnJ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;AAC3H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,SAAkB,EAAE,OAAgB;IACxD,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC;IACzJ,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;AACjE,CAAC;AAMD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAiB,EAAE,QAAiB;IAC/D,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,sBAAsB,OAAO,kDAAkD,IAAI,SAAS,CAAC;IAC3G,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,wCAAwC,KAAK,EAAE,EAAE,CAAC;IAC1I,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,IAAI,6BAA6B,QAAQ,8BAA8B,KAAK,EAAE,EAAE,CAAC;IAC5J,IAAI,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,CAAY,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC5F,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,QAAQ,uBAAuB,KAAK,EAAE,EAAE,CAAC;AACrG,CAAC","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.\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);\n return order === undefined ? !installed.prerelease : order > 0;\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"]}
1
+ {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAA0C;IACrE,wEAAwE;IACxE,8CAA8C;IAC9C,YAAY,EAAE,OAAO;IACrB,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY,EAAE,OAAO;IACrB,2EAA2E;IAC3E,wDAAwD;IACxD,YAAY,EAAE,QAAQ;CACvB,CAAC;AASF,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,yHAAyH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnJ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;AAC3H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,SAAkB,EAAE,OAAgB;IACxD,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9J,OAAO,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;AAC7D,CAAC;AAMD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAiB,EAAE,QAAiB;IAC/D,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,sBAAsB,OAAO,kDAAkD,IAAI,SAAS,CAAC;IAC3G,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,IAAI,wCAAwC,KAAK,EAAE,EAAE,CAAC;IAC1I,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,IAAI,6BAA6B,QAAQ,8BAA8B,KAAK,EAAE,EAAE,CAAC;IAC5J,IAAI,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,CAAY,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC5F,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,QAAQ,uBAAuB,KAAK,EAAE,EAAE,CAAC;AACrG,CAAC","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.\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"]}
package/hooks/hooks.json CHANGED
@@ -6,7 +6,12 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "npx --no-install spec-harness hook claude",
9
+ "command": "node",
10
+ "args": [
11
+ "${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js",
12
+ "hook",
13
+ "claude"
14
+ ],
10
15
  "timeout": 60
11
16
  }
12
17
  ]
@@ -18,7 +23,12 @@
18
23
  "hooks": [
19
24
  {
20
25
  "type": "command",
21
- "command": "npx --no-install spec-harness hook claude",
26
+ "command": "node",
27
+ "args": [
28
+ "${CLAUDE_PROJECT_DIR}/node_modules/@descent-vtt/spec-harness/bin/spec-harness.js",
29
+ "hook",
30
+ "claude"
31
+ ],
22
32
  "timeout": 60
23
33
  }
24
34
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descent-vtt/spec-harness",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -57,9 +57,8 @@
57
57
  "test:coverage": "vitest run --coverage",
58
58
  "test:mutation": "stryker run stryker.core.config.mjs",
59
59
  "test:mutation:full": "stryker run",
60
- "prepublishOnly": "npm run clean && npm run build"
60
+ "prepublishOnly": "node -e \"if (process.env.GITHUB_ACTIONS !== 'true') { console.error('npm publish is refused outside GitHub Actions: a release is a v* tag, staged by CI with provenance.'); process.exit(1) }\" && npm run clean && npm run build"
61
61
  },
62
- "dependencies": {},
63
62
  "peerDependencies": {
64
63
  "@descent-vtt/spec-brief": ">=0.2.0",
65
64
  "@descent-vtt/spec-graph": ">=0.9.0",
@@ -74,8 +73,8 @@
74
73
  }
75
74
  },
76
75
  "devDependencies": {
77
- "@descent-vtt/spec-brief": "^0.1.0",
78
- "@descent-vtt/spec-guard": "^0.11.0",
76
+ "@descent-vtt/spec-brief": "^0.2.0",
77
+ "@descent-vtt/spec-guard": "^0.12.0",
79
78
  "@stryker-mutator/core": "^10.0.0",
80
79
  "@stryker-mutator/vitest-runner": "^10.0.0",
81
80
  "@types/node": "^22.20.4",
@@ -33,6 +33,10 @@ the brief next to the one that measured it.
33
33
  npx spec-brief archive <id> --dry-run --base <branch>
34
34
  ```
35
35
 
36
+ A protected file a signed ruling allows is accepted only when spec-brief loads
37
+ spec-harness's plugin; if the dry run refuses one, `npx spec-harness doctor`
38
+ says whether it does, and `init` adds it.
39
+
36
40
  Show the person the audit, the dry run and a short summary of what the round
37
41
  did and did not do. Archiving is theirs: they run `spec-brief archive <id>`
38
42
  and commit the result. Do not archive a brief yourself.