@descent-vtt/spec-harness 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +17 -0
  3. package/.mcp.json +12 -0
  4. package/CHANGELOG.md +46 -0
  5. package/README.md +127 -9
  6. package/bin/spec-harness.js +19 -0
  7. package/dist/audit.d.ts +69 -0
  8. package/dist/audit.js +93 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/branch.d.ts +26 -0
  11. package/dist/branch.js +100 -0
  12. package/dist/branch.js.map +1 -0
  13. package/dist/briefs.d.ts +49 -0
  14. package/dist/briefs.js +111 -0
  15. package/dist/briefs.js.map +1 -0
  16. package/dist/cli.d.ts +14 -0
  17. package/dist/cli.js +245 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/commands.d.ts +18 -0
  20. package/dist/commands.js +224 -0
  21. package/dist/commands.js.map +1 -0
  22. package/dist/config.d.ts +57 -0
  23. package/dist/config.js +167 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/context.d.ts +56 -0
  26. package/dist/context.js +125 -0
  27. package/dist/context.js.map +1 -0
  28. package/dist/document.d.ts +33 -0
  29. package/dist/document.js +9 -0
  30. package/dist/document.js.map +1 -0
  31. package/dist/fs.d.ts +29 -0
  32. package/dist/fs.js +85 -0
  33. package/dist/fs.js.map +1 -0
  34. package/dist/git.d.ts +57 -0
  35. package/dist/git.js +129 -0
  36. package/dist/git.js.map +1 -0
  37. package/dist/guard.d.ts +43 -0
  38. package/dist/guard.js +120 -0
  39. package/dist/guard.js.map +1 -0
  40. package/dist/hooks.d.ts +50 -0
  41. package/dist/hooks.js +109 -0
  42. package/dist/hooks.js.map +1 -0
  43. package/dist/index.d.ts +31 -0
  44. package/dist/index.js +20 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/junit.d.ts +30 -0
  47. package/dist/junit.js +166 -0
  48. package/dist/junit.js.map +1 -0
  49. package/dist/manifests.d.ts +52 -0
  50. package/dist/manifests.js +346 -0
  51. package/dist/manifests.js.map +1 -0
  52. package/dist/plugin.d.ts +45 -0
  53. package/dist/plugin.js +79 -0
  54. package/dist/plugin.js.map +1 -0
  55. package/dist/premises.d.ts +16 -0
  56. package/dist/premises.js +71 -0
  57. package/dist/premises.js.map +1 -0
  58. package/dist/probe.d.ts +108 -0
  59. package/dist/probe.js +203 -0
  60. package/dist/probe.js.map +1 -0
  61. package/dist/reader.d.ts +8 -0
  62. package/dist/reader.js +105 -0
  63. package/dist/reader.js.map +1 -0
  64. package/dist/round.d.ts +68 -0
  65. package/dist/round.js +339 -0
  66. package/dist/round.js.map +1 -0
  67. package/dist/rulings.d.ts +92 -0
  68. package/dist/rulings.js +160 -0
  69. package/dist/rulings.js.map +1 -0
  70. package/dist/sandbox.d.ts +28 -0
  71. package/dist/sandbox.js +110 -0
  72. package/dist/sandbox.js.map +1 -0
  73. package/dist/server.d.ts +13 -0
  74. package/dist/server.js +212 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/setup.d.ts +32 -0
  77. package/dist/setup.js +218 -0
  78. package/dist/setup.js.map +1 -0
  79. package/dist/siblings.d.ts +66 -0
  80. package/dist/siblings.js +134 -0
  81. package/dist/siblings.js.map +1 -0
  82. package/dist/types.d.ts +38 -0
  83. package/dist/types.js +3 -0
  84. package/dist/types.js.map +1 -0
  85. package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
  86. package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
  87. package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
  88. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
  89. package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
  90. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
  91. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  92. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  93. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  94. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  95. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  96. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  97. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  98. package/dist/vendor/spec-core/markdown/index.js +4 -0
  99. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  100. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  101. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  102. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  103. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  104. package/dist/vendor/spec-core/markdown/links.js +364 -0
  105. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  106. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  107. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  108. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  109. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  110. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  111. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  112. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  113. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  114. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  115. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  116. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  117. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  118. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  119. package/dist/vendor/spec-core/markdown/types.js +9 -0
  120. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  121. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  122. package/dist/vendor/spec-core/path/index.js +2 -0
  123. package/dist/vendor/spec-core/path/index.js.map +1 -0
  124. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  125. package/dist/vendor/spec-core/path/posix.js +150 -0
  126. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  127. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  128. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  129. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  130. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  131. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  132. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  133. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  134. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  135. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  136. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  137. package/dist/vendor/spec-core/pattern/index.js +4 -0
  138. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  139. package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
  140. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  141. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  142. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  143. package/dist/vendor/spec-core/text/index.js +2 -0
  144. package/dist/vendor/spec-core/text/index.js.map +1 -0
  145. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  146. package/dist/vendor/spec-core/text/lines.js +156 -0
  147. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  148. package/dist/versions.d.ts +42 -0
  149. package/dist/versions.js +57 -0
  150. package/dist/versions.js.map +1 -0
  151. package/dist/workspace.d.ts +77 -0
  152. package/dist/workspace.js +138 -0
  153. package/dist/workspace.js.map +1 -0
  154. package/hooks/hooks.json +28 -0
  155. package/package.json +86 -6
  156. package/skills/close-round/SKILL.md +38 -0
  157. package/skills/draft-brief/SKILL.md +64 -0
  158. package/skills/run-round/SKILL.md +46 -0
  159. package/skills/split-goal/SKILL.md +42 -0
  160. package/src/vendor/spec-core/LICENSE +21 -0
  161. package/src/vendor/spec-core/README.md +8 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * What an agent needs to start a round, and nothing it does not.
3
+ *
4
+ * The brief in full - it is the contract, and a summary of a contract is a
5
+ * different contract. Then what the brief implies but does not say: the scope
6
+ * as the guard will read it, the rulings already signed, the briefs it waits
7
+ * on, the rules spec-guard holds the scope's code to, and the documents the
8
+ * brief cites. The cited documents are the one part that can be large, so
9
+ * they fill what the budget leaves, in the order the brief cites them, and
10
+ * the rest are named rather than dropped silently.
11
+ */
12
+ import { sameId } from './branch.js';
13
+ function list(items, empty) {
14
+ return items.length === 0 ? `- ${empty}` : items.map((item) => `- \`${item}\``).join('\n');
15
+ }
16
+ function rulesSection(rules) {
17
+ if (!Array.isArray(rules)) {
18
+ return `The rules spec-guard holds this code to could not be read: ${rules.unavailable}. Treat every ADR as binding until they can.`;
19
+ }
20
+ if (rules.length === 0)
21
+ return 'spec-guard holds no rule over this scope.';
22
+ const byDocument = new Map();
23
+ for (const rule of rules)
24
+ byDocument.set(rule.document, [...(byDocument.get(rule.document) ?? []), rule]);
25
+ const parts = [];
26
+ for (const [document, found] of byDocument) {
27
+ parts.push(`### ${document}`, '');
28
+ for (const rule of found)
29
+ parts.push(`- line ${rule.line}: ${rule.description}${rule.reason === null ? '' : ` - ${rule.reason}`}`);
30
+ parts.push('');
31
+ }
32
+ return parts.join('\n').trimEnd();
33
+ }
34
+ /**
35
+ * A brief's title without the id it repeats, `012 - Rotate tokens` read as
36
+ * `Rotate tokens`. Only the brief's own id is taken off: `Fix - the login
37
+ * bug` is a title, not an id and a title.
38
+ */
39
+ function titleOf(brief) {
40
+ if (brief.title === null)
41
+ return null;
42
+ return brief.title.replace(/^\s*(\S+)\s+-\s+/, (whole, first) => (sameId(first, brief.id) ? '' : whole));
43
+ }
44
+ /** Renders the packet, filling the budget with cited documents in the order the brief cites them. */
45
+ export function renderContext(input) {
46
+ const { brief } = input;
47
+ const title = titleOf(brief);
48
+ const header = [
49
+ `# Round ${brief.id}${title === null ? '' : `: ${title}`}`,
50
+ '',
51
+ [
52
+ `Status ${brief.status ?? 'unknown'}`,
53
+ brief.wave === null ? null : `wave ${brief.wave}`,
54
+ input.branch === null ? null : `branch \`${input.branch}\``,
55
+ input.base === null ? null : `measured from \`${input.base}\``,
56
+ ]
57
+ .filter((part) => part !== null)
58
+ .join(' · '),
59
+ '',
60
+ ].join('\n');
61
+ const contract = ['## The contract', '', `\`${brief.file}\`, in full:`, '', '````markdown', input.briefText.trimEnd(), '````', ''].join('\n');
62
+ const rulings = input.rulings.length === 0
63
+ ? '- none'
64
+ : input.rulings.map((ruling) => `- ${ruling.id}, signed by ${ruling.signer}: ${ruling.paths.map((p) => `\`${p}\``).join(', ')}`).join('\n');
65
+ const scope = [
66
+ '## Scope, as the guard reads it',
67
+ '',
68
+ 'May write:',
69
+ list(brief.affectedFiles, 'nothing declared: every write is outside the scope'),
70
+ '',
71
+ 'Must not change without a ruling:',
72
+ list(brief.protectedFiles, 'nothing declared'),
73
+ '',
74
+ 'Rulings in force:',
75
+ rulings,
76
+ '',
77
+ ].join('\n');
78
+ const waiting = input.dependencies.map((dependency) => {
79
+ const state = dependency.phase === 'archived' ? 'archived, done' : `still ${dependency.status ?? 'live'}`;
80
+ return `- ${dependency.id}${dependency.title === null ? '' : ` ${dependency.title}`}: ${state}`;
81
+ });
82
+ const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\n'), ''].join('\n');
83
+ const rules = ['## Rules in force for this scope', '', rulesSection(input.rules), ''].join('\n');
84
+ const howTo = [
85
+ '## How this round works',
86
+ '',
87
+ '- Before writing a file, ask whether the round may: `spec-harness guard <path>`, or the `check_path` tool.',
88
+ '- A protected file is changed only under a ruling a person signs. If the round cannot be done without one, stop and ask: `spec-harness escalate --path <file> --reason <why>`, or the `request_escalation` tool. Do not work around it.',
89
+ '- Every box in the brief ends ticked, or with a note under it that starts with a disposition (`**Delegated`, `**Accepted debt`, `**Rejected`, or what the repository configures).',
90
+ '- When the work is done, run `spec-harness audit` and fix what it reports. The archive is a person\'s decision, not the round\'s.',
91
+ '',
92
+ ].join('\n');
93
+ const fixed = [header, contract, scope, dependencies, rules, howTo].join('\n');
94
+ const included = [];
95
+ const omitted = [];
96
+ const unresolved = [];
97
+ const documents = [];
98
+ let used = fixed.length;
99
+ for (const cited of input.cited) {
100
+ if (cited.text === null) {
101
+ unresolved.push(cited.path);
102
+ continue;
103
+ }
104
+ const heading = `### \`${cited.path}\`${cited.title === null ? '' : ` - ${cited.title}`}${cited.status === null ? '' : ` (${cited.status})`}`;
105
+ const block = [heading, '', '````markdown', cited.text.trimEnd(), '````', ''].join('\n');
106
+ if (used + block.length > input.budget) {
107
+ omitted.push(cited.path);
108
+ continue;
109
+ }
110
+ used += block.length;
111
+ included.push(cited.path);
112
+ documents.push(block);
113
+ }
114
+ const citedSection = ['## Documents the brief cites', ''];
115
+ if (documents.length === 0 && omitted.length === 0 && unresolved.length === 0)
116
+ citedSection.push('None.', '');
117
+ citedSection.push(...documents);
118
+ if (omitted.length > 0) {
119
+ citedSection.push(`Left out to stay within ${input.budget} characters; read them when the work reaches them:`, list(omitted, ''), '');
120
+ }
121
+ if (unresolved.length > 0)
122
+ citedSection.push('Cited but not found in the repository:', list(unresolved, ''), '');
123
+ return { markdown: `${fixed}\n${citedSection.join('\n').trimEnd()}\n`, included, omitted, unresolved };
124
+ }
125
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What an agent needs to start a round, and nothing it does not.\n *\n * The brief in full - it is the contract, and a summary of a contract is a\n * different contract. Then what the brief implies but does not say: the scope\n * as the guard will read it, the rulings already signed, the briefs it waits\n * on, the rules spec-guard holds the scope's code to, and the documents the\n * brief cites. The cited documents are the one part that can be large, so\n * they fill what the budget leaves, in the order the brief cites them, and\n * the rest are named rather than dropped silently.\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\nexport interface CitedDocument {\n readonly path: string;\n readonly title: string | null;\n readonly status: string | null;\n /** `null` when the link does not resolve to a readable file. */\n readonly text: string | null;\n}\n\nexport interface RuleInForce {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n}\n\nexport interface RulingInForce {\n readonly id: string;\n readonly paths: readonly string[];\n readonly signer: string;\n}\n\nexport interface ContextInput {\n readonly brief: BriefRow;\n readonly briefText: string;\n readonly dependencies: readonly BriefRow[];\n readonly cited: readonly CitedDocument[];\n /** The rules in force for the scope, or why they could not be read. */\n readonly rules: readonly RuleInForce[] | { readonly unavailable: string };\n readonly rulings: readonly RulingInForce[];\n readonly branch: string | null;\n readonly base: string | null;\n readonly budget: number;\n}\n\nexport interface ContextPacket {\n readonly markdown: string;\n /** Cited documents included whole. */\n readonly included: readonly string[];\n /** Cited documents named but left out for the budget. */\n readonly omitted: readonly string[];\n /** Links in the brief that resolve to nothing. */\n readonly unresolved: readonly string[];\n}\n\nfunction list(items: readonly string[], empty: string): string {\n return items.length === 0 ? `- ${empty}` : items.map((item) => `- \\`${item}\\``).join('\\n');\n}\n\nfunction rulesSection(rules: ContextInput['rules']): string {\n if (!Array.isArray(rules)) {\n return `The rules spec-guard holds this code to could not be read: ${(rules as { unavailable: string }).unavailable}. Treat every ADR as binding until they can.`;\n }\n if (rules.length === 0) return 'spec-guard holds no rule over this scope.';\n const byDocument = new Map<string, RuleInForce[]>();\n for (const rule of rules) byDocument.set(rule.document, [...(byDocument.get(rule.document) ?? []), rule]);\n const parts: string[] = [];\n for (const [document, found] of byDocument) {\n parts.push(`### ${document}`, '');\n for (const rule of found) parts.push(`- line ${rule.line}: ${rule.description}${rule.reason === null ? '' : ` - ${rule.reason}`}`);\n parts.push('');\n }\n return parts.join('\\n').trimEnd();\n}\n\n/**\n * A brief's title without the id it repeats, `012 - Rotate tokens` read as\n * `Rotate tokens`. Only the brief's own id is taken off: `Fix - the login\n * bug` is a title, not an id and a title.\n */\nfunction titleOf(brief: BriefRow): string | null {\n if (brief.title === null) return null;\n return brief.title.replace(/^\\s*(\\S+)\\s+-\\s+/, (whole, first: string) => (sameId(first, brief.id) ? '' : whole));\n}\n\n/** Renders the packet, filling the budget with cited documents in the order the brief cites them. */\nexport function renderContext(input: ContextInput): ContextPacket {\n const { brief } = input;\n const title = titleOf(brief);\n const header = [\n `# Round ${brief.id}${title === null ? '' : `: ${title}`}`,\n '',\n [\n `Status ${brief.status ?? 'unknown'}`,\n brief.wave === null ? null : `wave ${brief.wave}`,\n input.branch === null ? null : `branch \\`${input.branch}\\``,\n input.base === null ? null : `measured from \\`${input.base}\\``,\n ]\n .filter((part) => part !== null)\n .join(' · '),\n '',\n ].join('\\n');\n\n const contract = ['## The contract', '', `\\`${brief.file}\\`, in full:`, '', '````markdown', input.briefText.trimEnd(), '````', ''].join('\\n');\n\n const rulings =\n input.rulings.length === 0\n ? '- none'\n : input.rulings.map((ruling) => `- ${ruling.id}, signed by ${ruling.signer}: ${ruling.paths.map((p) => `\\`${p}\\``).join(', ')}`).join('\\n');\n const scope = [\n '## Scope, as the guard reads it',\n '',\n 'May write:',\n list(brief.affectedFiles, 'nothing declared: every write is outside the scope'),\n '',\n 'Must not change without a ruling:',\n list(brief.protectedFiles, 'nothing declared'),\n '',\n 'Rulings in force:',\n rulings,\n '',\n ].join('\\n');\n\n const waiting = input.dependencies.map((dependency) => {\n const state = dependency.phase === 'archived' ? 'archived, done' : `still ${dependency.status ?? 'live'}`;\n return `- ${dependency.id}${dependency.title === null ? '' : ` ${dependency.title}`}: ${state}`;\n });\n const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\\n'), ''].join('\\n');\n\n const rules = ['## Rules in force for this scope', '', rulesSection(input.rules), ''].join('\\n');\n\n const howTo = [\n '## How this round works',\n '',\n '- Before writing a file, ask whether the round may: `spec-harness guard <path>`, or the `check_path` tool.',\n '- A protected file is changed only under a ruling a person signs. If the round cannot be done without one, stop and ask: `spec-harness escalate --path <file> --reason <why>`, or the `request_escalation` tool. Do not work around it.',\n '- Every box in the brief ends ticked, or with a note under it that starts with a disposition (`**Delegated`, `**Accepted debt`, `**Rejected`, or what the repository configures).',\n '- When the work is done, run `spec-harness audit` and fix what it reports. The archive is a person\\'s decision, not the round\\'s.',\n '',\n ].join('\\n');\n\n const fixed = [header, contract, scope, dependencies, rules, howTo].join('\\n');\n const included: string[] = [];\n const omitted: string[] = [];\n const unresolved: string[] = [];\n const documents: string[] = [];\n let used = fixed.length;\n for (const cited of input.cited) {\n if (cited.text === null) {\n unresolved.push(cited.path);\n continue;\n }\n const heading = `### \\`${cited.path}\\`${cited.title === null ? '' : ` - ${cited.title}`}${cited.status === null ? '' : ` (${cited.status})`}`;\n const block = [heading, '', '````markdown', cited.text.trimEnd(), '````', ''].join('\\n');\n if (used + block.length > input.budget) {\n omitted.push(cited.path);\n continue;\n }\n used += block.length;\n included.push(cited.path);\n documents.push(block);\n }\n const citedSection = ['## Documents the brief cites', ''];\n if (documents.length === 0 && omitted.length === 0 && unresolved.length === 0) citedSection.push('None.', '');\n citedSection.push(...documents);\n if (omitted.length > 0) {\n citedSection.push(`Left out to stay within ${input.budget} characters; read them when the work reaches them:`, list(omitted, ''), '');\n }\n if (unresolved.length > 0) citedSection.push('Cited but not found in the repository:', list(unresolved, ''), '');\n\n return { markdown: `${fixed}\\n${citedSection.join('\\n').trimEnd()}\\n`, included, omitted, unresolved };\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What the harness reads out of a brief or a cited document, through
3
+ * spec-core's Markdown scanner so that code and comments are never mistaken
4
+ * for structure: fenced blocks with their info strings, the section a line
5
+ * sits in, a section's tables, the relative links a document cites, and a
6
+ * document's title and status.
7
+ */
8
+ import type { CodeBlock } from './probe.js';
9
+ import type { TableView } from './rulings.js';
10
+ export interface Citation {
11
+ readonly target: string;
12
+ /** 1-based. */
13
+ readonly line: number;
14
+ }
15
+ export interface SectionTables {
16
+ readonly tables: readonly TableView[];
17
+ /** 1-based line of the section's heading, or `null` when there is no such section. */
18
+ readonly headingLine: number | null;
19
+ /** 0-based index of the line after the section's last table row, or `null` when it has no table. */
20
+ readonly tableEndLine: number | null;
21
+ }
22
+ export interface DocumentReader {
23
+ codeBlocks(text: string): CodeBlock[];
24
+ sectionAt(text: string, line: number): string | null;
25
+ /** Every section a line sits in, outermost first: a `###` under a `##` is in both. */
26
+ sectionsAt(text: string, line: number): string[];
27
+ sectionTables(text: string, section: string): SectionTables;
28
+ citations(text: string): Citation[];
29
+ titleAndStatus(text: string): {
30
+ readonly title: string | null;
31
+ readonly status: string | null;
32
+ };
33
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What the harness reads out of a brief or a cited document, through
3
+ * spec-core's Markdown scanner so that code and comments are never mistaken
4
+ * for structure: fenced blocks with their info strings, the section a line
5
+ * sits in, a section's tables, the relative links a document cites, and a
6
+ * document's title and status.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * What the harness reads out of a brief or a cited document, through\n * spec-core's Markdown scanner so that code and comments are never mistaken\n * for structure: fenced blocks with their info strings, the section a line\n * sits in, a section's tables, the relative links a document cites, and a\n * document's title and status.\n */\n\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\n\nexport interface Citation {\n readonly target: string;\n /** 1-based. */\n readonly line: number;\n}\n\nexport interface SectionTables {\n readonly tables: readonly TableView[];\n /** 1-based line of the section's heading, or `null` when there is no such section. */\n readonly headingLine: number | null;\n /** 0-based index of the line after the section's last table row, or `null` when it has no table. */\n readonly tableEndLine: number | null;\n}\n\nexport interface DocumentReader {\n codeBlocks(text: string): CodeBlock[];\n sectionAt(text: string, line: number): string | null;\n /** Every section a line sits in, outermost first: a `###` under a `##` is in both. */\n sectionsAt(text: string, line: number): string[];\n sectionTables(text: string, section: string): SectionTables;\n citations(text: string): Citation[];\n titleAndStatus(text: string): { readonly title: string | null; readonly status: string | null };\n}\n"]}
package/dist/fs.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The edge where the harness meets the disk: the configuration, the state
3
+ * directory outside the work tree, and paths as the filesystem spells them.
4
+ */
5
+ import { type HarnessConfig } from './config.js';
6
+ /** Reads `.spec-harness.json` at the root, or the defaults when there is none. */
7
+ export declare function loadConfig(root: string): Promise<{
8
+ config: HarnessConfig;
9
+ file: string | null;
10
+ }>;
11
+ /**
12
+ * The real spelling of a path: links resolved and, on a filesystem that
13
+ * ignores case, the case the directory entries have. A path that does not
14
+ * exist yet is resolved through its nearest existing ancestor.
15
+ *
16
+ * This is what stops `SRC/db/schema.ts` from walking past a protection on
17
+ * `src/db/schema.ts` on Windows or macOS, where both name one file.
18
+ */
19
+ export declare function realSpelling(path: string): string;
20
+ /**
21
+ * A path as the repository names it - relative to the root, POSIX-separated,
22
+ * real spelling - or `null` when it lies outside the root.
23
+ */
24
+ export declare function repositoryPath(given: string, root: string, cwd: string): string | null;
25
+ /** The harness's own directory under the git common directory, created on demand (ADR-0003). */
26
+ export declare function stateDirectory(commonDir: string, ...parts: string[]): Promise<string>;
27
+ /** Writes a file whole or not at all: a temporary file renamed over the target. */
28
+ export declare function writeAtomic(file: string, content: string): Promise<void>;
29
+ export declare function readText(file: string): Promise<string | null>;
package/dist/fs.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The edge where the harness meets the disk: the configuration, the state
3
+ * directory outside the work tree, and paths as the filesystem spells them.
4
+ */
5
+ import { existsSync, realpathSync } from 'node:fs';
6
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
+ import { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig } from './config.js';
9
+ /** Reads `.spec-harness.json` at the root, or the defaults when there is none. */
10
+ export async function loadConfig(root) {
11
+ const file = join(root, CONFIG_FILE);
12
+ if (!existsSync(file))
13
+ return { config: DEFAULT_CONFIG, file: null };
14
+ let raw;
15
+ try {
16
+ raw = JSON.parse(await readFile(file, 'utf8'));
17
+ }
18
+ catch (error) {
19
+ throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${error.message}`);
20
+ }
21
+ return { config: parseConfig(raw), file };
22
+ }
23
+ /**
24
+ * The real spelling of a path: links resolved and, on a filesystem that
25
+ * ignores case, the case the directory entries have. A path that does not
26
+ * exist yet is resolved through its nearest existing ancestor.
27
+ *
28
+ * This is what stops `SRC/db/schema.ts` from walking past a protection on
29
+ * `src/db/schema.ts` on Windows or macOS, where both name one file.
30
+ */
31
+ export function realSpelling(path) {
32
+ const missing = [];
33
+ let current = resolve(path);
34
+ for (;;) {
35
+ try {
36
+ const real = realpathSync.native(current);
37
+ return missing.length === 0 ? real : join(real, ...missing.reverse());
38
+ }
39
+ catch {
40
+ const parent = dirname(current);
41
+ if (parent === current)
42
+ return resolve(path);
43
+ missing.push(current.slice(parent.length).replace(/^[\\/]+/, ''));
44
+ current = parent;
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * A path as the repository names it - relative to the root, POSIX-separated,
50
+ * real spelling - or `null` when it lies outside the root.
51
+ */
52
+ export function repositoryPath(given, root, cwd) {
53
+ const absolute = isAbsolute(given) ? given : resolve(cwd, given);
54
+ const real = realSpelling(absolute);
55
+ const realRoot = realSpelling(root);
56
+ const rel = relative(realRoot, real);
57
+ if (rel === '')
58
+ return '';
59
+ // `..env` at the root is inside it; only a `..` segment climbs out.
60
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
61
+ return null;
62
+ return rel.split(sep).join('/');
63
+ }
64
+ /** The harness's own directory under the git common directory, created on demand (ADR-0003). */
65
+ export async function stateDirectory(commonDir, ...parts) {
66
+ const directory = join(commonDir, 'spec-harness', ...parts);
67
+ await mkdir(directory, { recursive: true });
68
+ return directory;
69
+ }
70
+ /** Writes a file whole or not at all: a temporary file renamed over the target. */
71
+ export async function writeAtomic(file, content) {
72
+ await mkdir(dirname(file), { recursive: true });
73
+ const temporary = `${file}.${process.pid}.tmp`;
74
+ await writeFile(temporary, content, 'utf8');
75
+ await rename(temporary, file);
76
+ }
77
+ export async function readText(file) {
78
+ try {
79
+ return await readFile(file, 'utf8');
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ //# sourceMappingURL=fs.js.map
package/dist/fs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAExG,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACrE,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CAAC,GAAG,WAAW,uBAAwB,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1C,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;YAChC,IAAI,MAAM,KAAK,OAAO;gBAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7C,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;YAClE,OAAO,GAAG,MAAM,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,IAAY,EAAE,GAAW;IACrE,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC1B,oEAAoE;IACpE,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/E,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB,EAAE,GAAG,KAAe;IACxE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC,CAAC;IAC5D,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,OAAe;IAC7D,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IAC/C,MAAM,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n const real = realpathSync.native(current);\n return missing.length === 0 ? real : join(real, ...missing.reverse());\n } catch {\n const parent = dirname(current);\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length).replace(/^[\\\\/]+/, ''));\n current = parent;\n }\n }\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n if (rel === '') return '';\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
package/dist/git.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The edge where the harness meets git.
3
+ *
4
+ * Reads, except for two things ADR-0003 allows and bounds: a temporary
5
+ * worktree, added and always removed by the sandbox, and nothing else. Every
6
+ * call is an argument vector, never a shell string, so a path or a ref cannot
7
+ * become a command.
8
+ */
9
+ import type { FileChange } from './types.js';
10
+ export interface Run {
11
+ readonly code: number;
12
+ readonly stdout: string;
13
+ readonly stderr: string;
14
+ }
15
+ /** Runs git and answers whatever it answered; a missing git is a code of -1. */
16
+ export declare function git(args: readonly string[], cwd: string, input?: string): Promise<Run>;
17
+ export declare class GitError extends Error {
18
+ constructor(message: string);
19
+ }
20
+ /** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */
21
+ export declare function workTreeRoot(cwd: string): Promise<string | null>;
22
+ /** The git directory every worktree of this repository shares. */
23
+ export declare function commonDirectory(cwd: string): Promise<string | null>;
24
+ /** The checked-out branch's short name, or `null` on a detached head. */
25
+ export declare function currentBranch(cwd: string): Promise<string | null>;
26
+ /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */
27
+ export declare function remoteDefault(cwd: string): Promise<string | null>;
28
+ export declare function revision(ref: string, cwd: string): Promise<string | null>;
29
+ export declare function mergeBase(a: string, b: string, cwd: string): Promise<string | null>;
30
+ /** Parses `git diff --name-status -z` output. */
31
+ export declare function parseNameStatus(output: string): FileChange[];
32
+ /** The files changed from one revision to another, renames found. */
33
+ export declare function changes(from: string, to: string, cwd: string): Promise<FileChange[]>;
34
+ /** The files a commit would change: what is staged. */
35
+ export declare function stagedChanges(cwd: string): Promise<FileChange[]>;
36
+ /** A file's content at a revision, or `null` when it does not exist there. */
37
+ export declare function show(ref: string, path: string, cwd: string): Promise<string | null>;
38
+ /**
39
+ * The commit that last changed one line of a file, as of a revision, or of
40
+ * the working tree when `ref` is `null` - where a line nobody committed yet
41
+ * belongs to the all-zero commit.
42
+ */
43
+ export declare function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null>;
44
+ export interface Signature {
45
+ readonly good: boolean;
46
+ /** The allowed signer's principal, for a good signature. */
47
+ readonly principal: string | null;
48
+ readonly detail: string;
49
+ }
50
+ /**
51
+ * Whether a commit carries a good SSH signature by a principal in the given
52
+ * allowed-signers file. The file is passed in, never read from the commit's
53
+ * own tree: a round cannot vouch for itself (ADR-0006).
54
+ */
55
+ export declare function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature>;
56
+ export declare function addWorktree(path: string, rev: string, cwd: string): Promise<void>;
57
+ export declare function removeWorktree(path: string, cwd: string): Promise<boolean>;
package/dist/git.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The edge where the harness meets git.
3
+ *
4
+ * Reads, except for two things ADR-0003 allows and bounds: a temporary
5
+ * worktree, added and always removed by the sandbox, and nothing else. Every
6
+ * call is an argument vector, never a shell string, so a path or a ref cannot
7
+ * become a command.
8
+ */
9
+ import { execFile } from 'node:child_process';
10
+ /** Runs git and answers whatever it answered; a missing git is a code of -1. */
11
+ export function git(args, cwd, input) {
12
+ return new Promise((resolve) => {
13
+ const child = execFile('git', [...args], { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {
14
+ const code = error === null ? 0 : typeof error.code === 'number' ? (error.code) : -1;
15
+ resolve({ code, stdout, stderr });
16
+ });
17
+ if (input !== undefined)
18
+ child.stdin?.end(input);
19
+ });
20
+ }
21
+ async function value(args, cwd) {
22
+ const run = await git(args, cwd);
23
+ return run.code === 0 ? run.stdout.trim() : null;
24
+ }
25
+ export class GitError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'GitError';
29
+ }
30
+ }
31
+ /** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */
32
+ export async function workTreeRoot(cwd) {
33
+ return value(['rev-parse', '--show-toplevel'], cwd);
34
+ }
35
+ /** The git directory every worktree of this repository shares. */
36
+ export async function commonDirectory(cwd) {
37
+ return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
38
+ }
39
+ /** The checked-out branch's short name, or `null` on a detached head. */
40
+ export async function currentBranch(cwd) {
41
+ return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);
42
+ }
43
+ /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */
44
+ export async function remoteDefault(cwd) {
45
+ return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);
46
+ }
47
+ export async function revision(ref, cwd) {
48
+ return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);
49
+ }
50
+ export async function mergeBase(a, b, cwd) {
51
+ return value(['merge-base', a, b], cwd);
52
+ }
53
+ /** Parses `git diff --name-status -z` output. */
54
+ export function parseNameStatus(output) {
55
+ const fields = output.split('\0');
56
+ const out = [];
57
+ for (let i = 0; i < fields.length;) {
58
+ const code = fields[i] ?? '';
59
+ if (code === '') {
60
+ i += 1;
61
+ continue;
62
+ }
63
+ const status = code.charAt(0);
64
+ if (status === 'R' || status === 'C') {
65
+ out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });
66
+ i += 3;
67
+ }
68
+ else {
69
+ out.push({ status, path: fields[i + 1] ?? '' });
70
+ i += 2;
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ /** The files changed from one revision to another, renames found. */
76
+ export async function changes(from, to, cwd) {
77
+ const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);
78
+ if (run.code !== 0)
79
+ throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);
80
+ return parseNameStatus(run.stdout);
81
+ }
82
+ /** The files a commit would change: what is staged. */
83
+ export async function stagedChanges(cwd) {
84
+ const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);
85
+ if (run.code !== 0)
86
+ throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);
87
+ return parseNameStatus(run.stdout);
88
+ }
89
+ /** A file's content at a revision, or `null` when it does not exist there. */
90
+ export async function show(ref, path, cwd) {
91
+ const run = await git(['show', `${ref}:${path}`], cwd);
92
+ return run.code === 0 ? run.stdout : null;
93
+ }
94
+ /**
95
+ * The commit that last changed one line of a file, as of a revision, or of
96
+ * the working tree when `ref` is `null` - where a line nobody committed yet
97
+ * belongs to the all-zero commit.
98
+ */
99
+ export async function blameLine(ref, path, line, cwd) {
100
+ const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);
101
+ if (run.code !== 0)
102
+ return null;
103
+ const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];
104
+ return sha ?? null;
105
+ }
106
+ /**
107
+ * Whether a commit carries a good SSH signature by a principal in the given
108
+ * allowed-signers file. The file is passed in, never read from the commit's
109
+ * own tree: a round cannot vouch for itself (ADR-0006).
110
+ */
111
+ export async function verifyCommit(sha, allowedSignersFile, cwd) {
112
+ const run = await git(['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha], cwd);
113
+ const text = `${run.stderr}\n${run.stdout}`;
114
+ const principal = /Good "git" signature for (\S+)/.exec(text)?.[1] ?? null;
115
+ return { good: run.code === 0 && principal !== null, principal, detail: text.trim().split('\n')[0] ?? '' };
116
+ }
117
+ export async function addWorktree(path, rev, cwd) {
118
+ const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);
119
+ if (run.code !== 0)
120
+ throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);
121
+ }
122
+ export async function removeWorktree(path, cwd) {
123
+ const run = await git(['worktree', 'remove', '--force', path], cwd);
124
+ if (run.code === 0)
125
+ return true;
126
+ await git(['worktree', 'prune'], cwd);
127
+ return false;
128
+ }
129
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string, input?: string): Promise<Run> {\n return new Promise((resolve) => {\n const child = execFile(\n 'git',\n [...args],\n { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true },\n (error, stdout, stderr) => {\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? ((error as { code: number }).code) : -1;\n resolve({ code, stdout, stderr });\n },\n );\n if (input !== undefined) child.stdin?.end(input);\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] ?? '';\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n if (run.code !== 0) return null;\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\nexport interface Signature {\n readonly good: boolean;\n /** The allowed signer's principal, for a good signature. */\n readonly principal: string | null;\n readonly detail: string;\n}\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1] ?? null;\n return { good: run.code === 0 && principal !== null, principal, detail: text.trim().split('\\n')[0] ?? '' };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n const run = await git(['worktree', 'remove', '--force', path], cwd);\n if (run.code === 0) return true;\n await git(['worktree', 'prune'], cwd);\n return false;\n}\n"]}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * May this round write this file?
3
+ *
4
+ * The answer is read from the brief the round runs under: `protectedFiles`
5
+ * are refused unless a person has ruled otherwise, `affectedFiles` are the
6
+ * round's to write, and anything else is outside the scope the person
7
+ * approved. A guard is a guardrail, not a boundary - an agent that writes
8
+ * through a shell passes it - so the audit and spec-brief's archive are the
9
+ * gates, and this is what keeps an honest agent from needing them (ADR-0005).
10
+ */
11
+ import type { OutOfScope } from './config.js';
12
+ import type { BriefRow } from './types.js';
13
+ export type Verdict = 'allow' | 'warn' | 'ask' | 'deny';
14
+ export type Reason = 'outside-repository' | 'no-brief' | 'brief-file' | 'protected' | 'ruled' | 'unreadable-protection' | 'in-scope' | 'out-of-scope';
15
+ export interface Decision {
16
+ /** Repository-relative, or the path as given when it lies outside. */
17
+ readonly path: string;
18
+ readonly verdict: Verdict;
19
+ readonly reason: Reason;
20
+ /** The patterns or ruling that decided it. */
21
+ readonly because: readonly string[];
22
+ readonly message: string;
23
+ readonly hint: string;
24
+ }
25
+ /** A ruling whose signature has been verified: a person allowed these paths. */
26
+ export interface VerifiedRuling {
27
+ readonly id: string;
28
+ readonly paths: readonly string[];
29
+ readonly signer: string;
30
+ }
31
+ export interface GuardInput {
32
+ /** Repository-relative POSIX path, or `null` for a path outside the repository. */
33
+ readonly path: string | null;
34
+ /** The path as the caller gave it, for messages. */
35
+ readonly given: string;
36
+ readonly brief: BriefRow | null;
37
+ /** Why there is no brief, when there is none. */
38
+ readonly noBrief?: string | undefined;
39
+ readonly rulings: readonly VerifiedRuling[];
40
+ readonly outOfScope: OutOfScope;
41
+ }
42
+ /** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */
43
+ export declare function decide(input: GuardInput): Decision;