@descent-vtt/spec-harness 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +12 -0
- package/CHANGELOG.md +56 -0
- package/README.md +127 -9
- package/bin/spec-harness.js +19 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +93 -0
- package/dist/audit.js.map +1 -0
- package/dist/branch.d.ts +26 -0
- package/dist/branch.js +100 -0
- package/dist/branch.js.map +1 -0
- package/dist/briefs.d.ts +49 -0
- package/dist/briefs.js +111 -0
- package/dist/briefs.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +224 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +167 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +56 -0
- package/dist/context.js +125 -0
- package/dist/context.js.map +1 -0
- package/dist/document.d.ts +33 -0
- package/dist/document.js +9 -0
- package/dist/document.js.map +1 -0
- package/dist/fs.d.ts +29 -0
- package/dist/fs.js +85 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +57 -0
- package/dist/git.js +129 -0
- package/dist/git.js.map +1 -0
- package/dist/guard.d.ts +43 -0
- package/dist/guard.js +120 -0
- package/dist/guard.js.map +1 -0
- package/dist/hooks.d.ts +50 -0
- package/dist/hooks.js +109 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/junit.d.ts +30 -0
- package/dist/junit.js +166 -0
- package/dist/junit.js.map +1 -0
- package/dist/manifests.d.ts +52 -0
- package/dist/manifests.js +346 -0
- package/dist/manifests.js.map +1 -0
- package/dist/plugin.d.ts +45 -0
- package/dist/plugin.js +79 -0
- package/dist/plugin.js.map +1 -0
- package/dist/premises.d.ts +16 -0
- package/dist/premises.js +71 -0
- package/dist/premises.js.map +1 -0
- package/dist/probe.d.ts +108 -0
- package/dist/probe.js +203 -0
- package/dist/probe.js.map +1 -0
- package/dist/reader.d.ts +8 -0
- package/dist/reader.js +105 -0
- package/dist/reader.js.map +1 -0
- package/dist/round.d.ts +68 -0
- package/dist/round.js +339 -0
- package/dist/round.js.map +1 -0
- package/dist/rulings.d.ts +92 -0
- package/dist/rulings.js +160 -0
- package/dist/rulings.js.map +1 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +32 -0
- package/dist/setup.js +218 -0
- package/dist/setup.js.map +1 -0
- package/dist/siblings.d.ts +66 -0
- package/dist/siblings.js +134 -0
- package/dist/siblings.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/dist/versions.d.ts +42 -0
- package/dist/versions.js +57 -0
- package/dist/versions.js.map +1 -0
- package/dist/workspace.d.ts +77 -0
- package/dist/workspace.js +138 -0
- package/dist/workspace.js.map +1 -0
- package/hooks/hooks.json +28 -0
- package/package.json +85 -6
- package/skills/close-round/SKILL.md +38 -0
- package/skills/draft-brief/SKILL.md +64 -0
- package/skills/run-round/SKILL.md +46 -0
- package/skills/split-goal/SKILL.md +42 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/rulings.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalations an agent raises and the rulings a person signs.
|
|
3
|
+
*
|
|
4
|
+
* A brief says what a round may not touch. When the round cannot be done
|
|
5
|
+
* without touching it, the agent stops and asks; a person decides; and the
|
|
6
|
+
* decision becomes a row in the brief's rulings table:
|
|
7
|
+
*
|
|
8
|
+
* ```markdown
|
|
9
|
+
* ## Rulings
|
|
10
|
+
*
|
|
11
|
+
* | Ruling | Paths | Decision | Note |
|
|
12
|
+
* | --- | --- | --- | --- |
|
|
13
|
+
* | R-012-1 | `src/db/schema.ts` | allow | Add rotated_at; no other schema change. |
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* A row is text, and an agent can write text. What makes a row a ruling is
|
|
17
|
+
* the commit that last changed it: signed, with a key listed in the allowed
|
|
18
|
+
* signers file as the base branch has it (ADR-0006). The row states the
|
|
19
|
+
* decision; the signature says a person made it; the base branch says whose
|
|
20
|
+
* signatures count. A hash of the row, which the agent could compute as well
|
|
21
|
+
* as anyone, would say none of that.
|
|
22
|
+
*
|
|
23
|
+
* The request itself lives outside the work tree while it waits
|
|
24
|
+
* (`<git-common-dir>/spec-harness/escalations/`): it is in-flight state, and
|
|
25
|
+
* only its outcome belongs in the record (spec-core ADR-0005).
|
|
26
|
+
*/
|
|
27
|
+
function normal(text) {
|
|
28
|
+
return text.replace(/[*_`]/g, '').trim().toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/** The globs a paths cell names: backtick-quoted, or comma-separated. */
|
|
31
|
+
export function pathsOf(cell) {
|
|
32
|
+
const quoted = [...cell.matchAll(/`([^`]+)`/g)].map((match) => match[1].trim());
|
|
33
|
+
if (quoted.length > 0)
|
|
34
|
+
return quoted.filter((path) => path !== '');
|
|
35
|
+
return cell
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter((part) => part !== '');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The rulings in a brief's rulings section, from its tables. A table whose
|
|
42
|
+
* header does not name the columns is not a rulings table; a row that cannot
|
|
43
|
+
* be read is a problem to report, never a ruling.
|
|
44
|
+
*/
|
|
45
|
+
export function readRulings(tables) {
|
|
46
|
+
const rulings = [];
|
|
47
|
+
const problems = [];
|
|
48
|
+
for (const table of tables) {
|
|
49
|
+
const headers = table.headers.map(normal);
|
|
50
|
+
const at = (...names) => headers.findIndex((header) => names.includes(header));
|
|
51
|
+
const idColumn = at('ruling', 'id');
|
|
52
|
+
const pathsColumn = at('paths', 'path', 'files');
|
|
53
|
+
const decisionColumn = at('decision');
|
|
54
|
+
const noteColumn = at('note', 'notes', 'reason');
|
|
55
|
+
if (idColumn < 0 || pathsColumn < 0 || decisionColumn < 0)
|
|
56
|
+
continue;
|
|
57
|
+
for (const row of table.rows) {
|
|
58
|
+
const id = normal(row.cells[idColumn] ?? '').toUpperCase();
|
|
59
|
+
const paths = pathsOf(row.cells[pathsColumn] ?? '');
|
|
60
|
+
const decision = normal(row.cells[decisionColumn] ?? '');
|
|
61
|
+
if (id === '') {
|
|
62
|
+
problems.push({ line: row.line, message: 'a ruling row has no id' });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (paths.length === 0) {
|
|
66
|
+
problems.push({ line: row.line, message: `ruling ${id} names no path` });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (decision !== 'allow' && decision !== 'deny') {
|
|
70
|
+
problems.push({ line: row.line, message: `ruling ${id} decides "${decision}"; a decision is allow or deny` });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (rulings.some((ruling) => ruling.id === id)) {
|
|
74
|
+
problems.push({ line: row.line, message: `ruling ${id} appears twice` });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
rulings.push({ id, paths, decision, note: (row.cells[noteColumn] ?? '').trim(), line: row.line });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { rulings, problems };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The next free escalation id for a brief, given the ids already taken. The
|
|
84
|
+
* stem compares without case: `readRulings` reads ids in capitals, so a brief
|
|
85
|
+
* named `auth` has `R-AUTH-1` taken when this writes `R-auth-2`.
|
|
86
|
+
*/
|
|
87
|
+
export function nextId(prefix, brief, taken) {
|
|
88
|
+
let highest = 0;
|
|
89
|
+
const stem = `${prefix}-${brief}-`;
|
|
90
|
+
for (const id of taken) {
|
|
91
|
+
if (!id.toUpperCase().startsWith(stem.toUpperCase()))
|
|
92
|
+
continue;
|
|
93
|
+
const n = Number(id.slice(stem.length));
|
|
94
|
+
if (Number.isInteger(n) && n > highest)
|
|
95
|
+
highest = n;
|
|
96
|
+
}
|
|
97
|
+
return `${stem}${highest + 1}`;
|
|
98
|
+
}
|
|
99
|
+
/** The memo a person reads to rule: what, why, the choices, and what each costs. */
|
|
100
|
+
export function renderMemo(request) {
|
|
101
|
+
const lines = [
|
|
102
|
+
`# Escalation ${request.id}`,
|
|
103
|
+
'',
|
|
104
|
+
`Brief ${request.brief} (\`${request.briefFile}\`)${request.branch === null ? '' : ` on \`${request.branch}\``}${request.head === null ? '' : ` at \`${request.head.slice(0, 12)}\``}, raised ${request.created}.`,
|
|
105
|
+
'',
|
|
106
|
+
'## What the round needs to change',
|
|
107
|
+
'',
|
|
108
|
+
...request.paths.map((path) => `- \`${path}\``),
|
|
109
|
+
'',
|
|
110
|
+
'## Why',
|
|
111
|
+
'',
|
|
112
|
+
request.reason.trim(),
|
|
113
|
+
'',
|
|
114
|
+
];
|
|
115
|
+
if (request.options.length > 0) {
|
|
116
|
+
lines.push('## Options', '');
|
|
117
|
+
request.options.forEach((option, index) => lines.push(`${index + 1}. **${option.label}** - ${option.consequence}`));
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
if (request.recommendation !== null)
|
|
121
|
+
lines.push('## The agent recommends', '', request.recommendation.trim(), '');
|
|
122
|
+
lines.push('## To rule', '', `Allow: \`spec-harness rule ${request.id} --allow --note "<what exactly is allowed>"\``, `Refuse: \`spec-harness rule ${request.id} --deny --note "<why>"\``, '', 'Either writes a row into the brief\'s rulings table. Commit it signed (`git commit -S`) with a key the base branch lists in its allowed signers; an unsigned row allows nothing.', '');
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
function cell(text) {
|
|
126
|
+
return text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
127
|
+
}
|
|
128
|
+
/** The table row a ruling writes. */
|
|
129
|
+
export function renderRow(id, paths, decision, note) {
|
|
130
|
+
return `| ${id} | ${paths.map((path) => `\`${cell(path)}\``).join(', ')} | ${decision} | ${cell(note)} |`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The brief with a ruling row added: under the rulings section's table when
|
|
134
|
+
* there is one, or in a new section at the end. Every other line is left as
|
|
135
|
+
* it was; line endings are the brief's own.
|
|
136
|
+
*/
|
|
137
|
+
export function addRulingRow(text, section, row, where) {
|
|
138
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
139
|
+
const lines = text.split(/\r?\n/);
|
|
140
|
+
const trailing = lines.length > 0 && lines[lines.length - 1] === '' ? lines.pop() : undefined;
|
|
141
|
+
const header = ['| Ruling | Paths | Decision | Note |', '| --- | --- | --- | --- |'];
|
|
142
|
+
if (where.tableEndLine !== null) {
|
|
143
|
+
lines.splice(where.tableEndLine, 0, row);
|
|
144
|
+
}
|
|
145
|
+
else if (where.headingLine !== null) {
|
|
146
|
+
// A line of prose right under the table would read as one more row.
|
|
147
|
+
const next = lines[where.headingLine];
|
|
148
|
+
const gap = next !== undefined && next.trim() !== '' ? [''] : [];
|
|
149
|
+
lines.splice(where.headingLine, 0, '', ...header, row, ...gap);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
if (lines.length > 0 && lines[lines.length - 1]?.trim() !== '')
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(`## ${section}`, '', ...header, row);
|
|
155
|
+
}
|
|
156
|
+
if (trailing !== undefined)
|
|
157
|
+
lines.push(trailing);
|
|
158
|
+
return lines.join(eol);
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=rulings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rulings.js","sourceRoot":"","sources":["../src/rulings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Escalations an agent raises and the rulings a person signs.\n *\n * A brief says what a round may not touch. When the round cannot be done\n * without touching it, the agent stops and asks; a person decides; and the\n * decision becomes a row in the brief's rulings table:\n *\n * ```markdown\n * ## Rulings\n *\n * | Ruling | Paths | Decision | Note |\n * | --- | --- | --- | --- |\n * | R-012-1 | `src/db/schema.ts` | allow | Add rotated_at; no other schema change. |\n * ```\n *\n * A row is text, and an agent can write text. What makes a row a ruling is\n * the commit that last changed it: signed, with a key listed in the allowed\n * signers file as the base branch has it (ADR-0006). The row states the\n * decision; the signature says a person made it; the base branch says whose\n * signatures count. A hash of the row, which the agent could compute as well\n * as anyone, would say none of that.\n *\n * The request itself lives outside the work tree while it waits\n * (`<git-common-dir>/spec-harness/escalations/`): it is in-flight state, and\n * only its outcome belongs in the record (spec-core ADR-0005).\n */\n\nexport interface EscalationOption {\n readonly label: string;\n readonly consequence: string;\n}\n\nexport interface EscalationRequest {\n readonly id: string;\n readonly brief: string;\n readonly briefFile: string;\n readonly paths: readonly string[];\n readonly reason: string;\n readonly options: readonly EscalationOption[];\n readonly recommendation: string | null;\n readonly created: string;\n readonly branch: string | null;\n readonly head: string | null;\n}\n\nexport interface RulingRow {\n readonly id: string;\n readonly paths: readonly string[];\n readonly decision: 'allow' | 'deny';\n readonly note: string;\n /** 1-based line of the row in the brief. */\n readonly line: number;\n}\n\n/** A table as the Markdown scanner reads it: header cells, and rows with their lines. */\nexport interface TableView {\n readonly headers: readonly string[];\n readonly rows: readonly { readonly cells: readonly string[]; readonly line: number }[];\n}\n\nexport type RulingProblem = { readonly line: number; readonly message: string };\n\nfunction normal(text: string): string {\n return text.replace(/[*_`]/g, '').trim().toLowerCase();\n}\n\n/** The globs a paths cell names: backtick-quoted, or comma-separated. */\nexport function pathsOf(cell: string): string[] {\n const quoted = [...cell.matchAll(/`([^`]+)`/g)].map((match) => (match[1] as string).trim());\n if (quoted.length > 0) return quoted.filter((path) => path !== '');\n return cell\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '');\n}\n\n/**\n * The rulings in a brief's rulings section, from its tables. A table whose\n * header does not name the columns is not a rulings table; a row that cannot\n * be read is a problem to report, never a ruling.\n */\nexport function readRulings(tables: readonly TableView[]): { rulings: RulingRow[]; problems: RulingProblem[] } {\n const rulings: RulingRow[] = [];\n const problems: RulingProblem[] = [];\n for (const table of tables) {\n const headers = table.headers.map(normal);\n const at = (...names: string[]): number => headers.findIndex((header) => names.includes(header));\n const idColumn = at('ruling', 'id');\n const pathsColumn = at('paths', 'path', 'files');\n const decisionColumn = at('decision');\n const noteColumn = at('note', 'notes', 'reason');\n if (idColumn < 0 || pathsColumn < 0 || decisionColumn < 0) continue;\n for (const row of table.rows) {\n const id = normal(row.cells[idColumn] ?? '').toUpperCase();\n const paths = pathsOf(row.cells[pathsColumn] ?? '');\n const decision = normal(row.cells[decisionColumn] ?? '');\n if (id === '') {\n problems.push({ line: row.line, message: 'a ruling row has no id' });\n continue;\n }\n if (paths.length === 0) {\n problems.push({ line: row.line, message: `ruling ${id} names no path` });\n continue;\n }\n if (decision !== 'allow' && decision !== 'deny') {\n problems.push({ line: row.line, message: `ruling ${id} decides \"${decision}\"; a decision is allow or deny` });\n continue;\n }\n if (rulings.some((ruling) => ruling.id === id)) {\n problems.push({ line: row.line, message: `ruling ${id} appears twice` });\n continue;\n }\n rulings.push({ id, paths, decision, note: (row.cells[noteColumn] ?? '').trim(), line: row.line });\n }\n }\n return { rulings, problems };\n}\n\n/**\n * The next free escalation id for a brief, given the ids already taken. The\n * stem compares without case: `readRulings` reads ids in capitals, so a brief\n * named `auth` has `R-AUTH-1` taken when this writes `R-auth-2`.\n */\nexport function nextId(prefix: 'E' | 'R', brief: string, taken: readonly string[]): string {\n let highest = 0;\n const stem = `${prefix}-${brief}-`;\n for (const id of taken) {\n if (!id.toUpperCase().startsWith(stem.toUpperCase())) continue;\n const n = Number(id.slice(stem.length));\n if (Number.isInteger(n) && n > highest) highest = n;\n }\n return `${stem}${highest + 1}`;\n}\n\n/** The memo a person reads to rule: what, why, the choices, and what each costs. */\nexport function renderMemo(request: EscalationRequest): string {\n const lines = [\n `# Escalation ${request.id}`,\n '',\n `Brief ${request.brief} (\\`${request.briefFile}\\`)${request.branch === null ? '' : ` on \\`${request.branch}\\``}${request.head === null ? '' : ` at \\`${request.head.slice(0, 12)}\\``}, raised ${request.created}.`,\n '',\n '## What the round needs to change',\n '',\n ...request.paths.map((path) => `- \\`${path}\\``),\n '',\n '## Why',\n '',\n request.reason.trim(),\n '',\n ];\n if (request.options.length > 0) {\n lines.push('## Options', '');\n request.options.forEach((option, index) => lines.push(`${index + 1}. **${option.label}** - ${option.consequence}`));\n lines.push('');\n }\n if (request.recommendation !== null) lines.push('## The agent recommends', '', request.recommendation.trim(), '');\n lines.push(\n '## To rule',\n '',\n `Allow: \\`spec-harness rule ${request.id} --allow --note \"<what exactly is allowed>\"\\``,\n `Refuse: \\`spec-harness rule ${request.id} --deny --note \"<why>\"\\``,\n '',\n 'Either writes a row into the brief\\'s rulings table. Commit it signed (`git commit -S`) with a key the base branch lists in its allowed signers; an unsigned row allows nothing.',\n '',\n );\n return lines.join('\\n');\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The table row a ruling writes. */\nexport function renderRow(id: string, paths: readonly string[], decision: 'allow' | 'deny', note: string): string {\n return `| ${id} | ${paths.map((path) => `\\`${cell(path)}\\``).join(', ')} | ${decision} | ${cell(note)} |`;\n}\n\n/**\n * The brief with a ruling row added: under the rulings section's table when\n * there is one, or in a new section at the end. Every other line is left as\n * it was; line endings are the brief's own.\n */\nexport function addRulingRow(\n text: string,\n section: string,\n row: string,\n where: { readonly headingLine: number | null; readonly tableEndLine: number | null },\n): string {\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const lines = text.split(/\\r?\\n/);\n const trailing = lines.length > 0 && lines[lines.length - 1] === '' ? lines.pop() : undefined;\n const header = ['| Ruling | Paths | Decision | Note |', '| --- | --- | --- | --- |'];\n if (where.tableEndLine !== null) {\n lines.splice(where.tableEndLine, 0, row);\n } else if (where.headingLine !== null) {\n // A line of prose right under the table would read as one more row.\n const next = lines[where.headingLine];\n const gap = next !== undefined && next.trim() !== '' ? [''] : [];\n lines.splice(where.headingLine, 0, '', ...header, row, ...gap);\n } else {\n if (lines.length > 0 && lines[lines.length - 1]?.trim() !== '') lines.push('');\n lines.push(`## ${section}`, '', ...header, row);\n }\n if (trailing !== undefined) lines.push(trailing);\n return lines.join(eol);\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary worktrees: somewhere to run a command at a commit without
|
|
3
|
+
* touching the person's own checkout.
|
|
4
|
+
*
|
|
5
|
+
* The family's tools read git and never write it (spec-core ADR-0005). The one
|
|
6
|
+
* exception is this: a detached worktree under the system's temporary
|
|
7
|
+
* directory, added for one job and removed after it, whether the job
|
|
8
|
+
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
|
+
* branch, index, stash and working files are never touched; a failed round is
|
|
10
|
+
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*/
|
|
12
|
+
/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */
|
|
13
|
+
export declare function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T>;
|
|
14
|
+
export interface CommandRun {
|
|
15
|
+
/** `null` when the command was stopped at its timeout. */
|
|
16
|
+
readonly exitCode: number | null;
|
|
17
|
+
readonly output: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Runs a command line in a directory, output and errors interleaved, stopped
|
|
21
|
+
* with everything it started when it outlives `timeoutSeconds`.
|
|
22
|
+
*
|
|
23
|
+
* Through the shell, on purpose: a probe's command is a line from the
|
|
24
|
+
* repository's own brief - `npm test -- x` - which the person approved with
|
|
25
|
+
* the brief, exactly as CI runs the repository's own scripts. Nothing from
|
|
26
|
+
* outside the repository reaches it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun>;
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary worktrees: somewhere to run a command at a commit without
|
|
3
|
+
* touching the person's own checkout.
|
|
4
|
+
*
|
|
5
|
+
* The family's tools read git and never write it (spec-core ADR-0005). The one
|
|
6
|
+
* exception is this: a detached worktree under the system's temporary
|
|
7
|
+
* directory, added for one job and removed after it, whether the job
|
|
8
|
+
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
|
+
* branch, index, stash and working files are never touched; a failed round is
|
|
10
|
+
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
13
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { addWorktree, removeWorktree } from './git.js';
|
|
17
|
+
const live = new Map();
|
|
18
|
+
let installed = false;
|
|
19
|
+
/** Removes every live worktree synchronously: the only kind of work an exit handler may do. */
|
|
20
|
+
function sweep() {
|
|
21
|
+
for (const [directory, repository] of live) {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Removed below regardless; `git worktree prune` then forgets it.
|
|
27
|
+
}
|
|
28
|
+
rmSync(directory, { recursive: true, force: true });
|
|
29
|
+
try {
|
|
30
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: repository, stdio: 'ignore', windowsHide: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Nothing more can be done from an exit handler.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
live.clear();
|
|
37
|
+
}
|
|
38
|
+
function install() {
|
|
39
|
+
if (installed)
|
|
40
|
+
return;
|
|
41
|
+
installed = true;
|
|
42
|
+
process.once('exit', sweep);
|
|
43
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
44
|
+
process.once(signal, () => {
|
|
45
|
+
sweep();
|
|
46
|
+
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */
|
|
51
|
+
export async function withWorktree(repository, revision, work) {
|
|
52
|
+
install();
|
|
53
|
+
const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));
|
|
54
|
+
live.set(directory, repository);
|
|
55
|
+
try {
|
|
56
|
+
await addWorktree(directory, revision, repository);
|
|
57
|
+
return await work(directory);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await removeWorktree(directory, repository);
|
|
61
|
+
rmSync(directory, { recursive: true, force: true });
|
|
62
|
+
live.delete(directory);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Runs a command line in a directory, output and errors interleaved, stopped
|
|
67
|
+
* with everything it started when it outlives `timeoutSeconds`.
|
|
68
|
+
*
|
|
69
|
+
* Through the shell, on purpose: a probe's command is a line from the
|
|
70
|
+
* repository's own brief - `npm test -- x` - which the person approved with
|
|
71
|
+
* the brief, exactly as CI runs the repository's own scripts. Nothing from
|
|
72
|
+
* outside the repository reaches it.
|
|
73
|
+
*/
|
|
74
|
+
export function runCommand(line, cwd, timeoutSeconds) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const child = spawn(line, { cwd, shell: true, windowsHide: true, detached: process.platform !== 'win32', env: { ...process.env, CI: '1' } });
|
|
77
|
+
let output = '';
|
|
78
|
+
const keep = (chunk) => {
|
|
79
|
+
// Enough to find a signature in; a runaway log is not kept whole.
|
|
80
|
+
if (output.length < 4 * 1024 * 1024)
|
|
81
|
+
output += chunk.toString('utf8');
|
|
82
|
+
};
|
|
83
|
+
child.stdout?.on('data', keep);
|
|
84
|
+
child.stderr?.on('data', keep);
|
|
85
|
+
let stopped = false;
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
stopped = true;
|
|
88
|
+
if (child.pid === undefined)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
if (process.platform === 'win32')
|
|
92
|
+
execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
93
|
+
else
|
|
94
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
child.kill('SIGKILL');
|
|
98
|
+
}
|
|
99
|
+
}, timeoutSeconds * 1000);
|
|
100
|
+
child.on('error', (error) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
resolve({ exitCode: 127, output: `${output}${error.message}\n` });
|
|
103
|
+
});
|
|
104
|
+
child.on('close', (code) => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
resolve({ exitCode: stopped ? null : (code ?? 1), output });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Temporary worktrees: somewhere to run a command at a commit without\n * touching the person's own checkout.\n *\n * The family's tools read git and never write it (spec-core ADR-0005). The one\n * exception is this: a detached worktree under the system's temporary\n * directory, added for one job and removed after it, whether the job\n * succeeded, threw, or the process was interrupted (ADR-0003). The person's\n * branch, index, stash and working files are never touched; a failed round is\n * discarded by discarding its worktree, never by resetting theirs.\n */\n\nimport { execFileSync, spawn } from 'node:child_process';\nimport { mkdtempSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { addWorktree, removeWorktree } from './git.js';\n\nconst live = new Map<string, string>();\nlet installed = false;\n\n/** Removes every live worktree synchronously: the only kind of work an exit handler may do. */\nfunction sweep(): void {\n for (const [directory, repository] of live) {\n try {\n execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });\n } catch {\n // Removed below regardless; `git worktree prune` then forgets it.\n }\n rmSync(directory, { recursive: true, force: true });\n try {\n execFileSync('git', ['worktree', 'prune'], { cwd: repository, stdio: 'ignore', windowsHide: true });\n } catch {\n // Nothing more can be done from an exit handler.\n }\n }\n live.clear();\n}\n\nfunction install(): void {\n if (installed) return;\n installed = true;\n process.once('exit', sweep);\n for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {\n process.once(signal, () => {\n sweep();\n process.exit(signal === 'SIGINT' ? 130 : 143);\n });\n }\n}\n\n/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */\nexport async function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T> {\n install();\n const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));\n live.set(directory, repository);\n try {\n await addWorktree(directory, revision, repository);\n return await work(directory);\n } finally {\n await removeWorktree(directory, repository);\n rmSync(directory, { recursive: true, force: true });\n live.delete(directory);\n }\n}\n\nexport interface CommandRun {\n /** `null` when the command was stopped at its timeout. */\n readonly exitCode: number | null;\n readonly output: string;\n}\n\n/**\n * Runs a command line in a directory, output and errors interleaved, stopped\n * with everything it started when it outlives `timeoutSeconds`.\n *\n * Through the shell, on purpose: a probe's command is a line from the\n * repository's own brief - `npm test -- x` - which the person approved with\n * the brief, exactly as CI runs the repository's own scripts. Nothing from\n * outside the repository reaches it.\n */\nexport function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun> {\n return new Promise((resolve) => {\n const child = spawn(line, { cwd, shell: true, windowsHide: true, detached: process.platform !== 'win32', env: { ...process.env, CI: '1' } });\n let output = '';\n const keep = (chunk: Buffer): void => {\n // Enough to find a signature in; a runaway log is not kept whole.\n if (output.length < 4 * 1024 * 1024) output += chunk.toString('utf8');\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n let stopped = false;\n const timer = setTimeout(() => {\n stopped = true;\n if (child.pid === undefined) return;\n try {\n if (process.platform === 'win32') execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n else process.kill(-child.pid, 'SIGKILL');\n } catch {\n child.kill('SIGKILL');\n }\n }, timeoutSeconds * 1000);\n child.on('error', (error) => {\n clearTimeout(timer);\n resolve({ exitCode: 127, output: `${output}${error.message}\\n` });\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n resolve({ exitCode: stopped ? null : (code ?? 1), output });\n });\n });\n}\n"]}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-harness mcp`: the round's questions, served over the Model Context
|
|
3
|
+
* Protocol on stdio (ADR-0008). The protocol is spec-core's `jsonrpc` module;
|
|
4
|
+
* this file is the five tools, the four prompts, and nothing else. Every
|
|
5
|
+
* request reads the repository afresh: a brief edited a minute ago is the
|
|
6
|
+
* brief the next answer uses.
|
|
7
|
+
*/
|
|
8
|
+
import { type PromptDefinition, type ToolDefinition } from './vendor/spec-core/jsonrpc/index.js';
|
|
9
|
+
import { type CliIO, type Options, type Workspace } from './workspace.js';
|
|
10
|
+
export declare const INSTRUCTIONS: string;
|
|
11
|
+
export declare function tools(workspace: Workspace, env: CliIO['env']): ToolDefinition[];
|
|
12
|
+
export declare function prompts(): PromptDefinition[];
|
|
13
|
+
export declare function mcpCommand(options: Options, io: CliIO): Promise<number>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-harness mcp`: the round's questions, served over the Model Context
|
|
3
|
+
* Protocol on stdio (ADR-0008). The protocol is spec-core's `jsonrpc` module;
|
|
4
|
+
* this file is the five tools, the four prompts, and nothing else. Every
|
|
5
|
+
* request reads the repository afresh: a brief edited a minute ago is the
|
|
6
|
+
* brief the next answer uses.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { findActive } from './briefs.js';
|
|
11
|
+
import { briefIdFromBranch } from './branch.js';
|
|
12
|
+
import { createReader } from './reader.js';
|
|
13
|
+
import { buildContext, checkPaths, raiseEscalation, runAudit } from './round.js';
|
|
14
|
+
import { createMcpServer, serveLines, toolError, unknownArguments, } from './vendor/spec-core/jsonrpc/index.js';
|
|
15
|
+
import { describeActive, openWorkspace, version } from './workspace.js';
|
|
16
|
+
const reader = createReader();
|
|
17
|
+
export const INSTRUCTIONS = 'spec-harness keeps a round of work inside the brief a person approved. ' +
|
|
18
|
+
'Call start_round before working: it returns the brief, the scope you may write, what you must not touch, the rules in force and the documents the brief cites. ' +
|
|
19
|
+
'Call check_path before writing a file you are unsure of. A protected file needs a ruling: call request_escalation and stop until a person rules. ' +
|
|
20
|
+
'Call audit_round when the work is done, and fix what it reports; archiving is a person\'s decision. ' +
|
|
21
|
+
'For the architecture rules themselves, spec-guard\'s own server answers get_architectural_rules.';
|
|
22
|
+
const briefProperty = { type: 'string', description: 'The brief id. Omit to use the brief the branch or SPEC_BRIEF names.' };
|
|
23
|
+
async function round(workspace, env, id) {
|
|
24
|
+
if (id !== undefined && typeof id !== 'string')
|
|
25
|
+
return toolError('"brief" must be a string.');
|
|
26
|
+
const briefs = await workspace.siblings.briefs();
|
|
27
|
+
const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);
|
|
28
|
+
const active = findActive(briefs, { flag: id, environment: env['SPEC_BRIEF'], branch: fromBranch });
|
|
29
|
+
const { brief, note, problem } = describeActive(active);
|
|
30
|
+
if (problem !== null)
|
|
31
|
+
return toolError(problem);
|
|
32
|
+
if (brief === null)
|
|
33
|
+
return toolError(note ?? 'no brief is named');
|
|
34
|
+
return { brief, briefs };
|
|
35
|
+
}
|
|
36
|
+
export function tools(workspace, env) {
|
|
37
|
+
const readOnly = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
descriptor: {
|
|
41
|
+
name: 'start_round',
|
|
42
|
+
title: 'Start a round',
|
|
43
|
+
description: 'Everything needed to start the round: the brief in full, the files it may write and must not change, signed rulings, the briefs it depends on, the architecture rules in force for its scope, and the documents it cites.',
|
|
44
|
+
inputSchema: { type: 'object', properties: { brief: briefProperty }, additionalProperties: false },
|
|
45
|
+
annotations: readOnly,
|
|
46
|
+
},
|
|
47
|
+
async call(args) {
|
|
48
|
+
const unknown = unknownArguments(args, ['brief']);
|
|
49
|
+
if (unknown)
|
|
50
|
+
return unknown;
|
|
51
|
+
const found = await round(workspace, env, args['brief']);
|
|
52
|
+
if ('text' in found)
|
|
53
|
+
return found;
|
|
54
|
+
const packet = await buildContext(workspace, found.brief, found.briefs, reader);
|
|
55
|
+
return { text: packet.markdown, structured: { brief: found.brief.id, included: [...packet.included], omitted: [...packet.omitted], unresolved: [...packet.unresolved] } };
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
descriptor: {
|
|
60
|
+
name: 'check_path',
|
|
61
|
+
title: 'May the round write these files',
|
|
62
|
+
description: 'For each path: allowed (in scope, or covered by a signed ruling), outside the scope, or refused (protected by the brief). Works for files that do not exist yet.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: { paths: { type: 'array', items: { type: 'string' }, description: 'Paths relative to the project root, or absolute inside it.' }, brief: briefProperty },
|
|
66
|
+
required: ['paths'],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
annotations: readOnly,
|
|
70
|
+
},
|
|
71
|
+
async call(args) {
|
|
72
|
+
const unknown = unknownArguments(args, ['paths', 'brief']);
|
|
73
|
+
if (unknown)
|
|
74
|
+
return unknown;
|
|
75
|
+
const paths = args['paths'];
|
|
76
|
+
if (!Array.isArray(paths) || !paths.every((p) => typeof p === 'string') || paths.length === 0) {
|
|
77
|
+
return toolError('"paths" must be a non-empty array of strings.');
|
|
78
|
+
}
|
|
79
|
+
const found = await round(workspace, env, args['brief']);
|
|
80
|
+
if ('text' in found)
|
|
81
|
+
return found;
|
|
82
|
+
const decisions = await checkPaths(workspace, found.brief, undefined, paths, workspace.root, reader);
|
|
83
|
+
const text = decisions.map((d) => `${d.verdict}: ${d.message}${d.verdict === 'allow' ? '' : `. Next: ${d.hint}`}`).join('\n');
|
|
84
|
+
return { text, structured: { decisions: decisions.map((d) => ({ ...d })) } };
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
descriptor: {
|
|
89
|
+
name: 'request_escalation',
|
|
90
|
+
title: 'Ask a person to rule',
|
|
91
|
+
description: 'When the round cannot be finished without changing something the brief protects, record the request and stop. A person reads the memo and rules; only a ruling they sign lets the change through.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'The protected files the round needs to change.' },
|
|
96
|
+
reason: { type: 'string', description: 'Why the round cannot be done without them.' },
|
|
97
|
+
options: {
|
|
98
|
+
type: 'array',
|
|
99
|
+
items: { type: 'object', properties: { label: { type: 'string' }, consequence: { type: 'string' } }, required: ['label', 'consequence'] },
|
|
100
|
+
description: 'The choices the person has, each with what it costs.',
|
|
101
|
+
},
|
|
102
|
+
recommendation: { type: 'string', description: 'Which option you recommend, and why.' },
|
|
103
|
+
brief: briefProperty,
|
|
104
|
+
},
|
|
105
|
+
required: ['paths', 'reason'],
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
|
|
109
|
+
},
|
|
110
|
+
async call(args) {
|
|
111
|
+
const unknown = unknownArguments(args, ['paths', 'reason', 'options', 'recommendation', 'brief']);
|
|
112
|
+
if (unknown)
|
|
113
|
+
return unknown;
|
|
114
|
+
const paths = args['paths'];
|
|
115
|
+
const reason = args['reason'];
|
|
116
|
+
if (!Array.isArray(paths) || !paths.every((p) => typeof p === 'string') || paths.length === 0)
|
|
117
|
+
return toolError('"paths" must be a non-empty array of strings.');
|
|
118
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
119
|
+
return toolError('"reason" is required.');
|
|
120
|
+
const rawOptions = args['options'] ?? [];
|
|
121
|
+
if (!Array.isArray(rawOptions))
|
|
122
|
+
return toolError('"options" must be an array.');
|
|
123
|
+
const options = rawOptions.map((o) => ({ label: String(o['label'] ?? ''), consequence: String(o['consequence'] ?? '') }));
|
|
124
|
+
const recommendation = args['recommendation'];
|
|
125
|
+
if (recommendation !== undefined && typeof recommendation !== 'string')
|
|
126
|
+
return toolError('"recommendation" must be a string.');
|
|
127
|
+
const found = await round(workspace, env, args['brief']);
|
|
128
|
+
if ('text' in found)
|
|
129
|
+
return found;
|
|
130
|
+
const { request, memo } = await raiseEscalation(workspace, found.brief, { paths: paths, reason, options, recommendation: recommendation ?? null });
|
|
131
|
+
return { text: `${memo}\nStop here until a person rules on ${request.id}.`, structured: { id: request.id } };
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
descriptor: {
|
|
136
|
+
name: 'audit_round',
|
|
137
|
+
title: 'Audit the round',
|
|
138
|
+
description: 'Did the round stay inside the lines: what the archive would refuse (open boxes, protected files, files outside the scope), the brief\'s own assertions, unverified rulings, and every dependency added.',
|
|
139
|
+
inputSchema: { type: 'object', properties: { brief: briefProperty, base: { type: 'string', description: 'The branch the round is measured from.' } }, additionalProperties: false },
|
|
140
|
+
annotations: readOnly,
|
|
141
|
+
},
|
|
142
|
+
async call(args) {
|
|
143
|
+
const unknown = unknownArguments(args, ['brief', 'base']);
|
|
144
|
+
if (unknown)
|
|
145
|
+
return unknown;
|
|
146
|
+
const base = args['base'];
|
|
147
|
+
if (base !== undefined && typeof base !== 'string')
|
|
148
|
+
return toolError('"base" must be a string.');
|
|
149
|
+
const found = await round(workspace, env, args['brief']);
|
|
150
|
+
if ('text' in found)
|
|
151
|
+
return found;
|
|
152
|
+
const result = await runAudit(workspace, found.brief, reader, base);
|
|
153
|
+
const { counts, findings } = result.report;
|
|
154
|
+
const text = findings.length === 0
|
|
155
|
+
? 'The audit found nothing.'
|
|
156
|
+
: findings.map((f) => `${f.severity} ${f.rule}: ${f.message}. Next: ${f.hint}`).join('\n');
|
|
157
|
+
return { text: `${text}\n\n${counts.error} error(s), ${counts.warning} warning(s), ${counts.note} note(s)`, structured: { counts, findings: findings.map((f) => ({ ...f })) } };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
descriptor: {
|
|
162
|
+
name: 'list_rounds',
|
|
163
|
+
title: 'List the rounds',
|
|
164
|
+
description: 'The live briefs: status, wave, what each waits on, and which are ready to start.',
|
|
165
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
166
|
+
annotations: readOnly,
|
|
167
|
+
},
|
|
168
|
+
async call(args) {
|
|
169
|
+
const unknown = unknownArguments(args, []);
|
|
170
|
+
if (unknown)
|
|
171
|
+
return unknown;
|
|
172
|
+
const live = (await workspace.siblings.briefs()).filter((brief) => brief.phase === 'live');
|
|
173
|
+
const text = live
|
|
174
|
+
.map((b) => `${b.id} ${b.title ?? ''} - ${b.status ?? 'unknown'}, wave ${b.wave ?? '-'}, ${b.ready ? 'ready' : `waits on ${b.waitingOn.join(', ')}`}`)
|
|
175
|
+
.join('\n');
|
|
176
|
+
return { text: text === '' ? 'No live brief.' : text, structured: { briefs: live.map((b) => ({ ...b })) } };
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
const SKILLS = [
|
|
182
|
+
{ name: 'draft-brief', title: 'Draft a brief', argument: 'request', description: 'What the round should achieve, in the person\'s words.' },
|
|
183
|
+
{ name: 'split-goal', title: 'Split a goal into rounds', argument: 'goal', description: 'The goal to split.' },
|
|
184
|
+
{ name: 'run-round', title: 'Run a round', argument: 'brief', description: 'The brief id.' },
|
|
185
|
+
{ name: 'close-round', title: 'Close a round', argument: 'brief', description: 'The brief id.' },
|
|
186
|
+
];
|
|
187
|
+
function skillText(name) {
|
|
188
|
+
const file = fileURLToPath(new URL(`../skills/${name}/SKILL.md`, import.meta.url));
|
|
189
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
190
|
+
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim();
|
|
191
|
+
}
|
|
192
|
+
export function prompts() {
|
|
193
|
+
return SKILLS.map((skill) => ({
|
|
194
|
+
descriptor: {
|
|
195
|
+
name: skill.name,
|
|
196
|
+
title: skill.title,
|
|
197
|
+
arguments: [{ name: skill.argument, description: skill.description, required: false }],
|
|
198
|
+
},
|
|
199
|
+
async get(args) {
|
|
200
|
+
const given = args[skill.argument];
|
|
201
|
+
const text = `${skillText(skill.name)}${given === undefined || given === '' ? '' : `\n\n${skill.argument}: ${given}`}`;
|
|
202
|
+
return { description: skill.title, messages: [{ role: 'user', content: { type: 'text', text } }] };
|
|
203
|
+
},
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
export async function mcpCommand(options, io) {
|
|
207
|
+
const workspace = await openWorkspace(options, io);
|
|
208
|
+
const handle = createMcpServer({ name: 'spec-harness', version: version(), instructions: INSTRUCTIONS, tools: tools(workspace, io.env), prompts: prompts() });
|
|
209
|
+
await serveLines(process.stdin, (line) => process.stdout.write(`${line}\n`), handle);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness mcp`: the round's questions, served over the Model Context\n * Protocol on stdio (ADR-0008). The protocol is spec-core's `jsonrpc` module;\n * this file is the five tools, the four prompts, and nothing else. Every\n * request reads the repository afresh: a brief edited a minute ago is the\n * brief the next answer uses.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\nimport { findActive } from './briefs.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { createReader } from './reader.js';\nimport { buildContext, checkPaths, raiseEscalation, runAudit } from './round.js';\nimport type { BriefRow } from './types.js';\nimport {\n createMcpServer,\n serveLines,\n toolError,\n unknownArguments,\n type JsonObject,\n type PromptDefinition,\n type ToolDefinition,\n type ToolOutcome,\n} from './vendor/spec-core/jsonrpc/index.js';\nimport { describeActive, openWorkspace, version, type CliIO, type Options, type Workspace } from './workspace.js';\n\nconst reader = createReader();\n\nexport const INSTRUCTIONS =\n 'spec-harness keeps a round of work inside the brief a person approved. ' +\n 'Call start_round before working: it returns the brief, the scope you may write, what you must not touch, the rules in force and the documents the brief cites. ' +\n 'Call check_path before writing a file you are unsure of. A protected file needs a ruling: call request_escalation and stop until a person rules. ' +\n 'Call audit_round when the work is done, and fix what it reports; archiving is a person\\'s decision. ' +\n 'For the architecture rules themselves, spec-guard\\'s own server answers get_architectural_rules.';\n\nconst briefProperty = { type: 'string', description: 'The brief id. Omit to use the brief the branch or SPEC_BRIEF names.' };\n\nasync function round(workspace: Workspace, env: CliIO['env'], id: unknown): Promise<{ brief: BriefRow; briefs: BriefRow[] } | ToolOutcome> {\n if (id !== undefined && typeof id !== 'string') return toolError('\"brief\" must be a string.');\n const briefs = await workspace.siblings.briefs();\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: id, environment: env['SPEC_BRIEF'], branch: fromBranch });\n const { brief, note, problem } = describeActive(active);\n if (problem !== null) return toolError(problem);\n if (brief === null) return toolError(note ?? 'no brief is named');\n return { brief, briefs };\n}\n\nexport function tools(workspace: Workspace, env: CliIO['env']): ToolDefinition[] {\n const readOnly = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };\n return [\n {\n descriptor: {\n name: 'start_round',\n title: 'Start a round',\n description:\n 'Everything needed to start the round: the brief in full, the files it may write and must not change, signed rulings, the briefs it depends on, the architecture rules in force for its scope, and the documents it cites.',\n inputSchema: { type: 'object', properties: { brief: briefProperty }, additionalProperties: false },\n annotations: readOnly,\n },\n async call(args: JsonObject): Promise<ToolOutcome> {\n const unknown = unknownArguments(args, ['brief']);\n if (unknown) return unknown;\n const found = await round(workspace, env, args['brief']);\n if ('text' in found) return found;\n const packet = await buildContext(workspace, found.brief, found.briefs, reader);\n return { text: packet.markdown, structured: { brief: found.brief.id, included: [...packet.included], omitted: [...packet.omitted], unresolved: [...packet.unresolved] } };\n },\n },\n {\n descriptor: {\n name: 'check_path',\n title: 'May the round write these files',\n description:\n 'For each path: allowed (in scope, or covered by a signed ruling), outside the scope, or refused (protected by the brief). Works for files that do not exist yet.',\n inputSchema: {\n type: 'object',\n properties: { paths: { type: 'array', items: { type: 'string' }, description: 'Paths relative to the project root, or absolute inside it.' }, brief: briefProperty },\n required: ['paths'],\n additionalProperties: false,\n },\n annotations: readOnly,\n },\n async call(args: JsonObject): Promise<ToolOutcome> {\n const unknown = unknownArguments(args, ['paths', 'brief']);\n if (unknown) return unknown;\n const paths = args['paths'];\n if (!Array.isArray(paths) || !paths.every((p) => typeof p === 'string') || paths.length === 0) {\n return toolError('\"paths\" must be a non-empty array of strings.');\n }\n const found = await round(workspace, env, args['brief']);\n if ('text' in found) return found;\n const decisions = await checkPaths(workspace, found.brief, undefined, paths as string[], workspace.root, reader);\n const text = decisions.map((d) => `${d.verdict}: ${d.message}${d.verdict === 'allow' ? '' : `. Next: ${d.hint}`}`).join('\\n');\n return { text, structured: { decisions: decisions.map((d) => ({ ...d })) } };\n },\n },\n {\n descriptor: {\n name: 'request_escalation',\n title: 'Ask a person to rule',\n description:\n 'When the round cannot be finished without changing something the brief protects, record the request and stop. A person reads the memo and rules; only a ruling they sign lets the change through.',\n inputSchema: {\n type: 'object',\n properties: {\n paths: { type: 'array', items: { type: 'string' }, description: 'The protected files the round needs to change.' },\n reason: { type: 'string', description: 'Why the round cannot be done without them.' },\n options: {\n type: 'array',\n items: { type: 'object', properties: { label: { type: 'string' }, consequence: { type: 'string' } }, required: ['label', 'consequence'] },\n description: 'The choices the person has, each with what it costs.',\n },\n recommendation: { type: 'string', description: 'Which option you recommend, and why.' },\n brief: briefProperty,\n },\n required: ['paths', 'reason'],\n additionalProperties: false,\n },\n annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },\n },\n async call(args: JsonObject): Promise<ToolOutcome> {\n const unknown = unknownArguments(args, ['paths', 'reason', 'options', 'recommendation', 'brief']);\n if (unknown) return unknown;\n const paths = args['paths'];\n const reason = args['reason'];\n if (!Array.isArray(paths) || !paths.every((p) => typeof p === 'string') || paths.length === 0) return toolError('\"paths\" must be a non-empty array of strings.');\n if (typeof reason !== 'string' || reason.trim() === '') return toolError('\"reason\" is required.');\n const rawOptions = args['options'] ?? [];\n if (!Array.isArray(rawOptions)) return toolError('\"options\" must be an array.');\n const options = rawOptions.map((o) => ({ label: String((o as JsonObject)['label'] ?? ''), consequence: String((o as JsonObject)['consequence'] ?? '') }));\n const recommendation = args['recommendation'];\n if (recommendation !== undefined && typeof recommendation !== 'string') return toolError('\"recommendation\" must be a string.');\n const found = await round(workspace, env, args['brief']);\n if ('text' in found) return found;\n const { request, memo } = await raiseEscalation(workspace, found.brief, { paths: paths as string[], reason, options, recommendation: recommendation ?? null });\n return { text: `${memo}\\nStop here until a person rules on ${request.id}.`, structured: { id: request.id } };\n },\n },\n {\n descriptor: {\n name: 'audit_round',\n title: 'Audit the round',\n description:\n 'Did the round stay inside the lines: what the archive would refuse (open boxes, protected files, files outside the scope), the brief\\'s own assertions, unverified rulings, and every dependency added.',\n inputSchema: { type: 'object', properties: { brief: briefProperty, base: { type: 'string', description: 'The branch the round is measured from.' } }, additionalProperties: false },\n annotations: readOnly,\n },\n async call(args: JsonObject): Promise<ToolOutcome> {\n const unknown = unknownArguments(args, ['brief', 'base']);\n if (unknown) return unknown;\n const base = args['base'];\n if (base !== undefined && typeof base !== 'string') return toolError('\"base\" must be a string.');\n const found = await round(workspace, env, args['brief']);\n if ('text' in found) return found;\n const result = await runAudit(workspace, found.brief, reader, base);\n const { counts, findings } = result.report;\n const text =\n findings.length === 0\n ? 'The audit found nothing.'\n : findings.map((f) => `${f.severity} ${f.rule}: ${f.message}. Next: ${f.hint}`).join('\\n');\n return { text: `${text}\\n\\n${counts.error} error(s), ${counts.warning} warning(s), ${counts.note} note(s)`, structured: { counts, findings: findings.map((f) => ({ ...f })) } };\n },\n },\n {\n descriptor: {\n name: 'list_rounds',\n title: 'List the rounds',\n description: 'The live briefs: status, wave, what each waits on, and which are ready to start.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: readOnly,\n },\n async call(args: JsonObject): Promise<ToolOutcome> {\n const unknown = unknownArguments(args, []);\n if (unknown) return unknown;\n const live = (await workspace.siblings.briefs()).filter((brief) => brief.phase === 'live');\n const text = live\n .map((b) => `${b.id} ${b.title ?? ''} - ${b.status ?? 'unknown'}, wave ${b.wave ?? '-'}, ${b.ready ? 'ready' : `waits on ${b.waitingOn.join(', ')}`}`)\n .join('\\n');\n return { text: text === '' ? 'No live brief.' : text, structured: { briefs: live.map((b) => ({ ...b })) } };\n },\n },\n ];\n}\n\nconst SKILLS: readonly { name: string; title: string; argument: string; description: string }[] = [\n { name: 'draft-brief', title: 'Draft a brief', argument: 'request', description: 'What the round should achieve, in the person\\'s words.' },\n { name: 'split-goal', title: 'Split a goal into rounds', argument: 'goal', description: 'The goal to split.' },\n { name: 'run-round', title: 'Run a round', argument: 'brief', description: 'The brief id.' },\n { name: 'close-round', title: 'Close a round', argument: 'brief', description: 'The brief id.' },\n];\n\nfunction skillText(name: string): string {\n const file = fileURLToPath(new URL(`../skills/${name}/SKILL.md`, import.meta.url));\n const text = existsSync(file) ? readFileSync(file, 'utf8') : '';\n return text.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n/, '').trim();\n}\n\nexport function prompts(): PromptDefinition[] {\n return SKILLS.map((skill) => ({\n descriptor: {\n name: skill.name,\n title: skill.title,\n arguments: [{ name: skill.argument, description: skill.description, required: false }],\n },\n async get(args: Readonly<Record<string, string>>): Promise<JsonObject> {\n const given = args[skill.argument];\n const text = `${skillText(skill.name)}${given === undefined || given === '' ? '' : `\\n\\n${skill.argument}: ${given}`}`;\n return { description: skill.title, messages: [{ role: 'user', content: { type: 'text', text } }] };\n },\n }));\n}\n\nexport async function mcpCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const handle = createMcpServer({ name: 'spec-harness', version: version(), instructions: INSTRUCTIONS, tools: tools(workspace, io.env), prompts: prompts() });\n await serveLines(process.stdin, (line) => process.stdout.write(`${line}\\n`), handle);\n return 0;\n}\n"]}
|