thoth-agents 0.2.11 → 0.3.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.
Files changed (149) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +21 -0
  3. package/README.md +301 -311
  4. package/dist/agents/configured-role-prompt.d.ts +9 -0
  5. package/dist/agents/prompt-sections.d.ts +8 -4
  6. package/dist/agents/prompt-utils.d.ts +0 -4
  7. package/dist/{chunk-ASKXWU6E.js → chunk-2MZ5C2RN.js} +1078 -1725
  8. package/dist/chunk-XO74DXBZ.js +1781 -0
  9. package/dist/{chunk-6BNAV534.js → chunk-YUCGIJPP.js} +12 -30
  10. package/dist/cli/claude-code-install.d.ts +24 -26
  11. package/dist/cli/claude-code-paths.d.ts +0 -28
  12. package/dist/cli/codex-install.d.ts +2 -2
  13. package/dist/cli/codex-paths.d.ts +0 -2
  14. package/dist/cli/index.js +279 -119
  15. package/dist/cli/install.d.ts +5 -1
  16. package/dist/cli/model-catalog/cache.d.ts +1 -1
  17. package/dist/cli/operations/claude-code.d.ts +6 -3
  18. package/dist/cli/operations/codex.d.ts +2 -1
  19. package/dist/cli/operations/opencode.d.ts +2 -1
  20. package/dist/cli/operations/types.d.ts +9 -1
  21. package/dist/cli/package-root.d.ts +1 -0
  22. package/dist/cli/providers.d.ts +6 -116
  23. package/dist/cli/skills.d.ts +19 -32
  24. package/dist/cli/thoth-mem-install.d.ts +45 -0
  25. package/dist/cli/tui/components/StatusView.d.ts +12 -1
  26. package/dist/cli/tui/index.js +256 -142
  27. package/dist/cli/tui/operations.d.ts +4 -3
  28. package/dist/cli/types.d.ts +0 -10
  29. package/dist/config/constants.d.ts +20 -16
  30. package/dist/config/index.d.ts +0 -1
  31. package/dist/config/loader.d.ts +1 -1
  32. package/dist/config/schema.d.ts +2 -38
  33. package/dist/config/utils.d.ts +1 -2
  34. package/dist/harness/adapters/codex-surfaces.d.ts +3 -3
  35. package/dist/harness/core/agent-pack.d.ts +37 -29
  36. package/dist/harness/core/memory-governance.d.ts +24 -48
  37. package/dist/harness/core/sdd.d.ts +364 -131
  38. package/dist/harness/generate-integration-packages.d.ts +20 -0
  39. package/dist/harness/opencode-init-command.d.ts +11 -0
  40. package/dist/harness/types.d.ts +14 -1
  41. package/dist/harness/writers/claude-code-subagent.d.ts +3 -1
  42. package/dist/hooks/index.d.ts +0 -5
  43. package/dist/index.js +562 -1314
  44. package/dist/mcp/index.d.ts +1 -2
  45. package/package.json +17 -12
  46. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +50 -0
  47. package/plugin/.claude-plugin/plugin.json +8 -0
  48. package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +16 -0
  49. package/plugin/.codex-plugin/plugin.json +7 -0
  50. package/plugin/.mcp.json +19 -0
  51. package/plugin/agents/deep.md +89 -0
  52. package/plugin/agents/designer.md +89 -0
  53. package/plugin/agents/explorer.md +90 -0
  54. package/plugin/agents/librarian.md +90 -0
  55. package/plugin/agents/oracle.md +92 -0
  56. package/plugin/agents/orchestrator.md +128 -0
  57. package/plugin/agents/quick.md +89 -0
  58. package/plugin/codex.mcp.json +17 -0
  59. package/plugin/settings.json +3 -0
  60. package/plugin/skills/thoth-archive/SKILL.md +40 -0
  61. package/plugin/skills/thoth-archive/scripts/archive.mjs +748 -0
  62. package/plugin/skills/thoth-constitution/SKILL.md +45 -0
  63. package/plugin/skills/thoth-constitution/scripts/validate.mjs +167 -0
  64. package/plugin/skills/thoth-constitution/templates/constitution.md +54 -0
  65. package/plugin/skills/thoth-init/SKILL.md +30 -0
  66. package/plugin/skills/thoth-init/scripts/init.mjs +141 -0
  67. package/plugin/skills/thoth-sdd/SKILL.md +83 -0
  68. package/plugin/skills/thoth-sdd/references/phases/analyze.md +21 -0
  69. package/plugin/skills/thoth-sdd/references/phases/checklist.md +20 -0
  70. package/plugin/skills/thoth-sdd/references/phases/clarify.md +15 -0
  71. package/plugin/skills/thoth-sdd/references/phases/converge.md +14 -0
  72. package/plugin/skills/thoth-sdd/references/phases/explore.md +10 -0
  73. package/plugin/skills/thoth-sdd/references/phases/implement.md +19 -0
  74. package/plugin/skills/thoth-sdd/references/phases/plan.md +15 -0
  75. package/plugin/skills/thoth-sdd/references/phases/specify.md +21 -0
  76. package/plugin/skills/thoth-sdd/references/phases/tasks.md +20 -0
  77. package/plugin/skills/thoth-sdd/references/phases/verify.md +22 -0
  78. package/plugin/skills/thoth-sdd/scripts/validate.mjs +1117 -0
  79. package/plugin/skills/thoth-sdd/templates/archive-report.md +25 -0
  80. package/plugin/skills/thoth-sdd/templates/checklist.md +27 -0
  81. package/plugin/skills/thoth-sdd/templates/plan.md +38 -0
  82. package/plugin/skills/thoth-sdd/templates/spec.md +54 -0
  83. package/plugin/skills/thoth-sdd/templates/tasks.md +29 -0
  84. package/plugin/skills/thoth-sdd/templates/verify-report.md +28 -0
  85. package/skills/README.md +12 -0
  86. package/skills/thoth-archive/SKILL.md +40 -0
  87. package/skills/thoth-archive/scripts/archive.mjs +748 -0
  88. package/skills/thoth-constitution/SKILL.md +45 -0
  89. package/skills/thoth-constitution/scripts/validate.mjs +167 -0
  90. package/skills/thoth-constitution/templates/constitution.md +54 -0
  91. package/skills/thoth-init/SKILL.md +30 -0
  92. package/skills/thoth-init/scripts/init.mjs +141 -0
  93. package/skills/thoth-sdd/SKILL.md +83 -0
  94. package/skills/thoth-sdd/references/phases/analyze.md +21 -0
  95. package/skills/thoth-sdd/references/phases/checklist.md +20 -0
  96. package/skills/thoth-sdd/references/phases/clarify.md +15 -0
  97. package/skills/thoth-sdd/references/phases/converge.md +14 -0
  98. package/skills/thoth-sdd/references/phases/explore.md +10 -0
  99. package/skills/thoth-sdd/references/phases/implement.md +19 -0
  100. package/skills/thoth-sdd/references/phases/plan.md +15 -0
  101. package/skills/thoth-sdd/references/phases/specify.md +21 -0
  102. package/skills/thoth-sdd/references/phases/tasks.md +20 -0
  103. package/skills/thoth-sdd/references/phases/verify.md +22 -0
  104. package/skills/thoth-sdd/scripts/validate.mjs +1117 -0
  105. package/skills/thoth-sdd/templates/archive-report.md +25 -0
  106. package/skills/thoth-sdd/templates/checklist.md +27 -0
  107. package/skills/thoth-sdd/templates/plan.md +38 -0
  108. package/skills/thoth-sdd/templates/spec.md +54 -0
  109. package/skills/thoth-sdd/templates/tasks.md +29 -0
  110. package/skills/thoth-sdd/templates/verify-report.md +28 -0
  111. package/thoth-agents.schema.json +0 -39
  112. package/dist/chunk-OZE7JVGS.js +0 -2344
  113. package/dist/cli/custom-skills.d.ts +0 -49
  114. package/dist/cli/skill-manifest.d.ts +0 -32
  115. package/dist/harness/core/skills.d.ts +0 -211
  116. package/dist/harness/generate-codex-plugin.d.ts +0 -9
  117. package/dist/harness/writers/claude-code-skill-layout.d.ts +0 -16
  118. package/dist/harness/writers/fs-skill-collect.d.ts +0 -7
  119. package/dist/harness/writers/skill-layout.d.ts +0 -16
  120. package/dist/hooks/chat-headers.d.ts +0 -16
  121. package/dist/hooks/phase-reminder/index.d.ts +0 -28
  122. package/dist/hooks/post-read-nudge/index.d.ts +0 -18
  123. package/dist/hooks/skill-sync.d.ts +0 -10
  124. package/dist/hooks/thoth-mem/index.d.ts +0 -46
  125. package/dist/hooks/thoth-mem/protocol.d.ts +0 -7
  126. package/dist/mcp/thoth.d.ts +0 -3
  127. package/dist/sdd/artifact-governance/artifact-loader.d.ts +0 -130
  128. package/dist/sdd/artifact-governance/index.d.ts +0 -6
  129. package/dist/sdd/artifact-governance/tasks-validator.d.ts +0 -17
  130. package/dist/sdd/artifact-governance/types.d.ts +0 -52
  131. package/dist/thoth/client.d.ts +0 -14
  132. package/dist/thoth/index.d.ts +0 -2
  133. package/src/skills/_shared/openspec-convention.md +0 -350
  134. package/src/skills/_shared/persistence-contract.md +0 -163
  135. package/src/skills/_shared/thoth-mem-convention.md +0 -123
  136. package/src/skills/executing-plans/SKILL.md +0 -288
  137. package/src/skills/plan-reviewer/SKILL.md +0 -219
  138. package/src/skills/requirements-interview/SKILL.md +0 -233
  139. package/src/skills/sdd-apply/SKILL.md +0 -89
  140. package/src/skills/sdd-archive/SKILL.md +0 -92
  141. package/src/skills/sdd-clarify/SKILL.md +0 -105
  142. package/src/skills/sdd-constitution/SKILL.md +0 -107
  143. package/src/skills/sdd-design/SKILL.md +0 -155
  144. package/src/skills/sdd-init/SKILL.md +0 -233
  145. package/src/skills/sdd-propose/SKILL.md +0 -101
  146. package/src/skills/sdd-spec/SKILL.md +0 -136
  147. package/src/skills/sdd-tasks/SKILL.md +0 -232
  148. package/src/skills/sdd-verify/SKILL.md +0 -131
  149. package/src/skills/thoth-mem-agents/SKILL.md +0 -210
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmdirSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { basename, dirname, join, resolve } from 'node:path';
13
+
14
+ const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
+ const TEST_FAULTS = new Set(
16
+ (process.env.THOTH_ARCHIVE_TEST_FAULT ?? '')
17
+ .split(',')
18
+ .map((fault) => fault.trim())
19
+ .filter(Boolean),
20
+ );
21
+
22
+ function injectFault(stage) {
23
+ if (TEST_FAULTS.has(stage)) {
24
+ throw new Error(`Injected archive fault: ${stage}`);
25
+ }
26
+ }
27
+
28
+ function errorMessage(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+
32
+ function attemptRecovery(errors, label, operation) {
33
+ try {
34
+ operation();
35
+ } catch (error) {
36
+ errors.push(`${label}: ${errorMessage(error)}`);
37
+ }
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ const options = { json: false };
42
+ for (let index = 0; index < argv.length; index += 1) {
43
+ const argument = argv[index];
44
+ if (argument === '--json') options.json = true;
45
+ else if (argument === '--change') options.change = argv[++index];
46
+ else if (argument === '--date') options.date = argv[++index];
47
+ else throw new Error(`Unknown argument: ${argument}`);
48
+ }
49
+ if (!options.change) throw new Error('--change is required');
50
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date ?? '')) {
51
+ throw new Error('--date must use YYYY-MM-DD');
52
+ }
53
+ return options;
54
+ }
55
+
56
+ function sectionContent(content, heading) {
57
+ const expression = new RegExp(`^##\\s+${heading}\\s*$`, 'im');
58
+ const match = expression.exec(content);
59
+ if (!match || match.index === undefined) return undefined;
60
+ const start = match.index + match[0].length;
61
+ const remainder = content.slice(start);
62
+ const nextHeading = /^##\s+/m.exec(remainder);
63
+ return remainder.slice(0, nextHeading?.index ?? remainder.length).trim();
64
+ }
65
+
66
+ function contractCandidateLines(content, prefix) {
67
+ const pattern = new RegExp(`^\\s*-\\s+(?:\\*\\*)?${prefix}\\b`, 'i');
68
+ return content.split(/\r?\n/).filter((line) => pattern.test(line));
69
+ }
70
+
71
+ function assertSequentialContractIds(ids, prefix) {
72
+ const valid =
73
+ new Set(ids).size === ids.length &&
74
+ ids.every(
75
+ (id, index) => id === `${prefix}-${String(index + 1).padStart(3, '0')}`,
76
+ );
77
+ if (!valid) {
78
+ throw new Error(`${prefix}-### identifiers must be unique and sequential`);
79
+ }
80
+ }
81
+
82
+ function parseComplianceMatrix(content) {
83
+ const matrix = sectionContent(content, 'Compliance matrix') ?? '';
84
+ const candidates = matrix
85
+ .split(/\r?\n/)
86
+ .filter((line) => /^\s*\|\s*(?:FR|SC)-/i.test(line));
87
+ const entries = [
88
+ ...matrix.matchAll(
89
+ /^\|\s*((?:FR|SC)-\d{3})(?:\s+`?\[(?:buildable|outcome)\]`?)?\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(PASS|RISK|FAIL)\s*\|\s*$/gim,
90
+ ),
91
+ ].map((match) => ({
92
+ id: match[1].toUpperCase(),
93
+ evidence: match[2],
94
+ check: match[3],
95
+ result: match[4].toUpperCase(),
96
+ }));
97
+ return { entries, malformed: candidates.length !== entries.length };
98
+ }
99
+
100
+ function isConcreteEvidence(value) {
101
+ const normalized = value.replaceAll('`', '').trim();
102
+ return (
103
+ normalized.length > 0 &&
104
+ !/^\[.*\]$/.test(normalized) &&
105
+ !/^(?:N\/?A|NONE|PENDING|TBD|NOT YET|-)[.!]?$/i.test(normalized)
106
+ );
107
+ }
108
+
109
+ function hasExplicitResidualRisk(content, id) {
110
+ const residualRisks = sectionContent(content, 'Residual risks') ?? '';
111
+ const match = new RegExp(`^-\\s+${id}\\s*:\\s*(\\S.*)$`, 'im').exec(
112
+ residualRisks,
113
+ );
114
+ return match !== null && isConcreteEvidence(match[1]);
115
+ }
116
+
117
+ function hasBlockingCriticalFinding(content) {
118
+ const findings = sectionContent(content, 'Findings') ?? '';
119
+ return findings.split(/\r?\n/).some((line) => {
120
+ if (!/\bCRITICAL\b/i.test(line)) return false;
121
+ if (/\bRESOLVED\b/i.test(line)) return false;
122
+ return !/\b(?:NO|ZERO)\s+(?:OPEN\s+)?CRITICAL\b/i.test(line);
123
+ });
124
+ }
125
+
126
+ function hasConcreteReviewDimensions(content) {
127
+ const dimensions = sectionContent(content, 'Review dimensions') ?? '';
128
+ return ['Completeness', 'Correctness', 'Coherence'].every((dimension) => {
129
+ const match = new RegExp(
130
+ `^-\\s*\\*\\*${dimension}\\*\\*:\\s*(\\S.*)$`,
131
+ 'im',
132
+ ).exec(dimensions);
133
+ return match !== null && isConcreteEvidence(match[1]);
134
+ });
135
+ }
136
+
137
+ function assertVerifyCloseout(verify, contract) {
138
+ if (
139
+ !/^\*\*Reviewer\*\*:\s*oracle(?:<br>)?\s*$/im.test(verify) ||
140
+ !/^\*\*Independent from implementer\*\*:\s*Yes(?:<br>)?\s*$/im.test(verify)
141
+ ) {
142
+ throw new Error(
143
+ 'verify-report.md requires an independent oracle reviewer before archive',
144
+ );
145
+ }
146
+ if (!/^\*\*Verdict\*\*:\s*PASS\s*$/im.test(verify)) {
147
+ throw new Error('verify-report.md must record PASS before archive');
148
+ }
149
+ if (hasBlockingCriticalFinding(verify)) {
150
+ throw new Error('Unresolved CRITICAL verification findings block archive');
151
+ }
152
+
153
+ if (!hasConcreteReviewDimensions(verify)) {
154
+ throw new Error(
155
+ 'verify-report.md must record completeness, correctness, and coherence review dimensions',
156
+ );
157
+ }
158
+
159
+ const matrix = parseComplianceMatrix(verify);
160
+ const knownIds = new Set([
161
+ ...contract.frIds,
162
+ ...contract.buildableScIds,
163
+ ...contract.outcomeScIds,
164
+ ]);
165
+ const rowIds = matrix.entries.map(({ id }) => id);
166
+ const duplicateIds = rowIds.filter(
167
+ (id, index) => rowIds.indexOf(id) !== index,
168
+ );
169
+ const unknownIds = rowIds.filter((id) => !knownIds.has(id));
170
+ const failedIds = matrix.entries
171
+ .filter(({ result }) => result === 'FAIL')
172
+ .map(({ id }) => id);
173
+ if (
174
+ matrix.malformed ||
175
+ duplicateIds.length > 0 ||
176
+ unknownIds.length > 0 ||
177
+ failedIds.length > 0
178
+ ) {
179
+ throw new Error(
180
+ 'Compliance matrix must contain canonical rows with unique known requirement IDs and no FAIL results',
181
+ );
182
+ }
183
+ const rows = new Map(matrix.entries.map((entry) => [entry.id, entry]));
184
+ const missingEvidence = [
185
+ ...contract.frIds,
186
+ ...contract.buildableScIds,
187
+ ].filter((id) => {
188
+ const row = rows.get(id);
189
+ return (
190
+ row?.result !== 'PASS' ||
191
+ !isConcreteEvidence(row.evidence) ||
192
+ !isConcreteEvidence(row.check)
193
+ );
194
+ });
195
+ if (missingEvidence.length > 0) {
196
+ throw new Error(
197
+ `Verification evidence is missing for: ${missingEvidence.join(', ')}`,
198
+ );
199
+ }
200
+
201
+ const unresolvedOutcomes = contract.outcomeScIds.filter((id) => {
202
+ const row = rows.get(id);
203
+ if (!row) return true;
204
+ if (row.result === 'PASS') {
205
+ return (
206
+ !isConcreteEvidence(row.evidence) || !isConcreteEvidence(row.check)
207
+ );
208
+ }
209
+ return row.result !== 'RISK' || !hasExplicitResidualRisk(verify, id);
210
+ });
211
+ if (unresolvedOutcomes.length > 0) {
212
+ throw new Error(
213
+ `Outcome verification disposition is missing for: ${unresolvedOutcomes.join(', ')}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ function assertArchivable(change, contract) {
219
+ for (const file of [
220
+ 'spec.md',
221
+ 'plan.md',
222
+ 'tasks.md',
223
+ 'verify-report.md',
224
+ 'archive-report.md',
225
+ ]) {
226
+ if (!existsSync(join(change, file))) {
227
+ throw new Error(`${file} is required before archive`);
228
+ }
229
+ }
230
+
231
+ const tasks = readFileSync(join(change, 'tasks.md'), 'utf8');
232
+ if (/^- \[(?: |~)\] T\d{3}\b/m.test(tasks)) {
233
+ throw new Error('All tasks must be complete before archive');
234
+ }
235
+
236
+ const verify = readFileSync(join(change, 'verify-report.md'), 'utf8');
237
+ assertVerifyCloseout(verify, contract);
238
+
239
+ const report = readFileSync(join(change, 'archive-report.md'), 'utf8');
240
+ if (!/^\*\*Status\*\*:\s*READY(?:<br>)?\s*$/im.test(report)) {
241
+ throw new Error('archive-report.md must record READY before archive');
242
+ }
243
+ if (!/^\*\*Oracle verdict\*\*:\s*PASS(?:<br>)?\s*$/im.test(report)) {
244
+ throw new Error('archive-report.md must record oracle PASS before archive');
245
+ }
246
+ if (
247
+ !/^\*\*Archive path\*\*:\s*`openspec\/changes\/archive\/YYYY-MM-DD-\[feature\]\/`(?:<br>)?\s*$/im.test(
248
+ report,
249
+ )
250
+ ) {
251
+ throw new Error('archive-report.md must retain the dated archive target');
252
+ }
253
+ if (
254
+ !/verify-report\.md/i.test(
255
+ sectionContent(report, 'Verification lineage') ?? '',
256
+ )
257
+ ) {
258
+ throw new Error('archive-report.md must retain verification lineage');
259
+ }
260
+ if (
261
+ !/^- Pending: archive applies declared durable deltas transactionally\.\s*$/im.test(
262
+ sectionContent(report, 'Canonical specification sync') ?? '',
263
+ )
264
+ ) {
265
+ throw new Error(
266
+ 'archive-report.md must include the pending canonical specification sync',
267
+ );
268
+ }
269
+ return report;
270
+ }
271
+
272
+ function parseDeltaMetadata(metadata) {
273
+ if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
274
+
275
+ const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
276
+ if (change && CAPABILITY_PATTERN.test(change[2])) {
277
+ return { operation: change[1], capability: change[2] };
278
+ }
279
+
280
+ const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
281
+ if (
282
+ rename &&
283
+ CAPABILITY_PATTERN.test(rename[1]) &&
284
+ rename[2].trim().length > 0
285
+ ) {
286
+ return {
287
+ operation: 'RENAMED',
288
+ capability: rename[1],
289
+ previousTitle: rename[2].trim(),
290
+ };
291
+ }
292
+
293
+ throw new Error(`Invalid requirement delta metadata: [${metadata}]`);
294
+ }
295
+
296
+ function parseStoryScenarios(spec) {
297
+ const userStories = sectionContent(spec, 'User stories');
298
+ if (userStories === undefined) {
299
+ throw new Error('spec.md must contain a canonical User stories section');
300
+ }
301
+ const stories = [
302
+ ...userStories.matchAll(
303
+ /^###\s+US(\d+)\s+-\s+(.+?)\s+\(Priority:\s*P\d+\)\s*$/gim,
304
+ ),
305
+ ];
306
+ const scenariosByRequirement = new Map();
307
+
308
+ for (const [index, storyMatch] of stories.entries()) {
309
+ const start = storyMatch.index ?? 0;
310
+ const nextStory = stories[index + 1]?.index;
311
+ const end = nextStory ?? userStories.length;
312
+ const story = userStories.slice(start, end);
313
+ const covers =
314
+ /^\*\*Covers\*\*:\s*(.+)$/im.exec(story)?.[1].match(/\bFR-\d{3}\b/g) ??
315
+ [];
316
+ const scenarios = [
317
+ ...story.matchAll(
318
+ /^\d+\.\s+\*\*Given\*\*\s+(.+?),\s+\*\*When\*\*\s+(.+?),\s+\*\*Then\*\*\s+(.+?)\.?\s*$/gim,
319
+ ),
320
+ ].map((scenario, scenarioIndex) => ({
321
+ title: `US${storyMatch[1]} - ${storyMatch[2].trim()} ${scenarioIndex + 1}`,
322
+ given: scenario[1].trim(),
323
+ when: scenario[2].trim(),
324
+ result: scenario[3].trim(),
325
+ }));
326
+
327
+ for (const requirementId of covers) {
328
+ const current = scenariosByRequirement.get(requirementId) ?? [];
329
+ current.push(...scenarios);
330
+ scenariosByRequirement.set(requirementId, current);
331
+ }
332
+ }
333
+
334
+ return scenariosByRequirement;
335
+ }
336
+
337
+ function parseChangeSpec(spec) {
338
+ const scenariosByRequirement = parseStoryScenarios(spec);
339
+ const functionalRequirementsSection = sectionContent(
340
+ spec,
341
+ 'Functional requirements',
342
+ );
343
+ const successCriteriaSection = sectionContent(spec, 'Success criteria');
344
+ if (functionalRequirementsSection === undefined) {
345
+ throw new Error(
346
+ 'spec.md must contain a canonical Functional requirements section',
347
+ );
348
+ }
349
+ if (successCriteriaSection === undefined) {
350
+ throw new Error(
351
+ 'spec.md must contain a canonical Success criteria section',
352
+ );
353
+ }
354
+ const functionalRequirements = [
355
+ ...functionalRequirementsSection.matchAll(
356
+ /^- \*\*(FR-\d{3})\s+—\s+(.+?)\*\*:\s*`\[(.+?)\]`\s+(\S.+)$/gim,
357
+ ),
358
+ ];
359
+ const rawFrCandidates = contractCandidateLines(spec, 'FR');
360
+ if (
361
+ functionalRequirements.length === 0 ||
362
+ rawFrCandidates.length !== functionalRequirements.length
363
+ ) {
364
+ throw new Error(
365
+ 'Every FR-### must use the canonical named requirement, delta metadata, and normative statement format',
366
+ );
367
+ }
368
+ assertSequentialContractIds(
369
+ functionalRequirements.map((match) => match[1]),
370
+ 'FR',
371
+ );
372
+
373
+ const successCriteria = [
374
+ ...successCriteriaSection.matchAll(
375
+ /^- \*\*(SC-\d{3})\*\*\s+`\[(buildable|outcome)\]`:\s*(\S.+)$/gim,
376
+ ),
377
+ ];
378
+ const rawScCandidates = contractCandidateLines(spec, 'SC');
379
+ if (
380
+ successCriteria.length === 0 ||
381
+ rawScCandidates.length !== successCriteria.length
382
+ ) {
383
+ throw new Error(
384
+ 'Every SC-### must be classified as buildable or outcome before archive',
385
+ );
386
+ }
387
+ assertSequentialContractIds(
388
+ successCriteria.map((match) => match[1]),
389
+ 'SC',
390
+ );
391
+
392
+ const deltas = [];
393
+ for (const match of functionalRequirements) {
394
+ const metadata = parseDeltaMetadata(match[3]);
395
+ if (metadata.operation === 'INTERNAL') continue;
396
+ const scenarios = scenariosByRequirement.get(match[1]) ?? [];
397
+ if (scenarios.length === 0) {
398
+ throw new Error(
399
+ `${match[1]} must map to at least one acceptance scenario before archive`,
400
+ );
401
+ }
402
+ deltas.push({
403
+ title: match[2].trim(),
404
+ statement: match[4].trim(),
405
+ scenarios,
406
+ ...metadata,
407
+ });
408
+ }
409
+ return {
410
+ deltas,
411
+ frIds: functionalRequirements.map((match) => match[1]),
412
+ buildableScIds: successCriteria
413
+ .filter((match) => match[2].toLowerCase() === 'buildable')
414
+ .map((match) => match[1]),
415
+ outcomeScIds: successCriteria
416
+ .filter((match) => match[2].toLowerCase() === 'outcome')
417
+ .map((match) => match[1]),
418
+ };
419
+ }
420
+
421
+ function parseCanonicalSpec(content) {
422
+ const headings = [...content.matchAll(/^### Requirement:\s+(.+?)\s*$/gm)];
423
+ const requirements = new Map();
424
+ const prefix =
425
+ headings.length === 0
426
+ ? content.trimEnd()
427
+ : content.slice(0, headings[0].index).trimEnd();
428
+
429
+ for (const [index, heading] of headings.entries()) {
430
+ const start = heading.index ?? 0;
431
+ const end = headings[index + 1]?.index ?? content.length;
432
+ const title = heading[1].trim();
433
+ if (requirements.has(title)) {
434
+ throw new Error(`Canonical specification repeats requirement: ${title}`);
435
+ }
436
+ requirements.set(title, content.slice(start, end).trim());
437
+ }
438
+
439
+ return { prefix, requirements };
440
+ }
441
+
442
+ function newCanonicalSpec(capability) {
443
+ const title = capability
444
+ .split('-')
445
+ .map((part) => part[0].toUpperCase() + part.slice(1))
446
+ .join(' ');
447
+ return parseCanonicalSpec(
448
+ `# ${title} Specification\n\n## Purpose\n\nDurable behavioral contract for \`${capability}\`.\n\n## Requirements\n`,
449
+ );
450
+ }
451
+
452
+ function renderRequirement(delta) {
453
+ const scenarios = delta.scenarios
454
+ .map(
455
+ (scenario) =>
456
+ `#### Scenario: ${scenario.title}\n\n- **GIVEN** ${scenario.given}\n- **WHEN** ${scenario.when}\n- **THEN** ${scenario.result}`,
457
+ )
458
+ .join('\n\n');
459
+ return `### Requirement: ${delta.title}\n\n${delta.statement}\n\n${scenarios}`;
460
+ }
461
+
462
+ function renderCanonicalSpec(canonical) {
463
+ const requirements = [...canonical.requirements.values()];
464
+ return `${canonical.prefix}${requirements.length > 0 ? `\n\n${requirements.join('\n\n')}` : ''}\n`;
465
+ }
466
+
467
+ function planCanonicalUpdates(specRoot, deltas) {
468
+ const byCapability = new Map();
469
+ for (const delta of deltas) {
470
+ const capability = delta.capability;
471
+ if (!CAPABILITY_PATTERN.test(capability)) {
472
+ throw new Error(`Invalid capability: ${capability}`);
473
+ }
474
+ const current = byCapability.get(capability) ?? [];
475
+ current.push(delta);
476
+ byCapability.set(capability, current);
477
+ }
478
+
479
+ const updates = [];
480
+ for (const [capability, capabilityDeltas] of byCapability) {
481
+ const path = join(specRoot, capability, 'spec.md');
482
+ const present = existsSync(path);
483
+ const canonical = present
484
+ ? parseCanonicalSpec(readFileSync(path, 'utf8'))
485
+ : newCanonicalSpec(capability);
486
+
487
+ for (const delta of capabilityDeltas) {
488
+ if (delta.operation === 'ADDED') {
489
+ if (canonical.requirements.has(delta.title)) {
490
+ throw new Error(
491
+ `${capability} already contains requirement: ${delta.title}`,
492
+ );
493
+ }
494
+ canonical.requirements.set(delta.title, renderRequirement(delta));
495
+ continue;
496
+ }
497
+
498
+ if (!present) {
499
+ throw new Error(
500
+ `${capability} has no canonical specification for ${delta.operation}`,
501
+ );
502
+ }
503
+
504
+ if (delta.operation === 'RENAMED') {
505
+ if (!canonical.requirements.has(delta.previousTitle)) {
506
+ throw new Error(
507
+ `${capability} does not contain requirement: ${delta.previousTitle}`,
508
+ );
509
+ }
510
+ if (
511
+ delta.previousTitle !== delta.title &&
512
+ canonical.requirements.has(delta.title)
513
+ ) {
514
+ throw new Error(
515
+ `${capability} already contains requirement: ${delta.title}`,
516
+ );
517
+ }
518
+ const entries = [...canonical.requirements.entries()];
519
+ canonical.requirements = new Map(
520
+ entries.map(([title, block]) =>
521
+ title === delta.previousTitle
522
+ ? [delta.title, renderRequirement(delta)]
523
+ : [title, block],
524
+ ),
525
+ );
526
+ continue;
527
+ }
528
+
529
+ if (!canonical.requirements.has(delta.title)) {
530
+ throw new Error(
531
+ `${capability} does not contain requirement: ${delta.title}`,
532
+ );
533
+ }
534
+ if (delta.operation === 'MODIFIED') {
535
+ canonical.requirements.set(delta.title, renderRequirement(delta));
536
+ } else if (delta.operation === 'REMOVED') {
537
+ canonical.requirements.delete(delta.title);
538
+ }
539
+ }
540
+
541
+ updates.push({ path, content: renderCanonicalSpec(canonical), capability });
542
+ }
543
+ return updates;
544
+ }
545
+
546
+ function stageCanonicalUpdates(updates) {
547
+ const transactionId = `${process.pid}-${Date.now()}`;
548
+ const staged = [];
549
+ const createdDirectories = [];
550
+ try {
551
+ for (const update of updates) {
552
+ const directory = dirname(update.path);
553
+ if (!existsSync(directory)) {
554
+ mkdirSync(directory, { recursive: true });
555
+ createdDirectories.push(directory);
556
+ }
557
+ const stagePath = join(
558
+ directory,
559
+ `.spec.md.thoth-stage-${transactionId}`,
560
+ );
561
+ writeFileSync(stagePath, update.content);
562
+ staged.push({ ...update, stagePath });
563
+ }
564
+ return { staged, createdDirectories, transactionId };
565
+ } catch (error) {
566
+ try {
567
+ rollbackCanonicalUpdates({ staged, createdDirectories, applied: [] });
568
+ } catch (recoveryError) {
569
+ throw new Error(
570
+ `${errorMessage(error)}; staging recovery failed: ${errorMessage(recoveryError)}`,
571
+ );
572
+ }
573
+ throw error;
574
+ }
575
+ }
576
+
577
+ function applyCanonicalUpdates(transaction) {
578
+ const applied = [];
579
+ try {
580
+ for (const item of transaction.staged) {
581
+ const backupPath = `${item.path}.thoth-backup-${transaction.transactionId}`;
582
+ const hadOriginal = existsSync(item.path);
583
+ if (hadOriginal) {
584
+ renameSync(item.path, backupPath);
585
+ }
586
+ applied.push({ ...item, backupPath, hadOriginal });
587
+ if (hadOriginal) injectFault('after-original-backup');
588
+ renameSync(item.stagePath, item.path);
589
+ if (applied.length === 1) injectFault('after-first-canonical-write');
590
+ }
591
+ return { ...transaction, applied };
592
+ } catch (error) {
593
+ try {
594
+ rollbackCanonicalUpdates({ ...transaction, applied });
595
+ } catch (recoveryError) {
596
+ throw new Error(
597
+ `${errorMessage(error)}; canonical recovery failed: ${errorMessage(recoveryError)}`,
598
+ );
599
+ }
600
+ throw error;
601
+ }
602
+ }
603
+
604
+ function rollbackCanonicalUpdates(transaction) {
605
+ const recoveryErrors = [];
606
+ for (const item of [...(transaction.applied ?? [])].reverse()) {
607
+ attemptRecovery(recoveryErrors, `remove ${item.path}`, () =>
608
+ rmSync(item.path, { force: true }),
609
+ );
610
+ if (item.hadOriginal) {
611
+ if (existsSync(item.backupPath)) {
612
+ attemptRecovery(recoveryErrors, `restore ${item.path}`, () =>
613
+ renameSync(item.backupPath, item.path),
614
+ );
615
+ } else {
616
+ recoveryErrors.push(`restore ${item.path}: backup is missing`);
617
+ }
618
+ }
619
+ }
620
+ for (const item of transaction.staged ?? []) {
621
+ attemptRecovery(recoveryErrors, `remove ${item.stagePath}`, () =>
622
+ rmSync(item.stagePath, { force: true }),
623
+ );
624
+ }
625
+ for (const directory of [
626
+ ...(transaction.createdDirectories ?? []),
627
+ ].reverse()) {
628
+ if (existsSync(directory)) {
629
+ attemptRecovery(recoveryErrors, `remove ${directory}`, () =>
630
+ rmdirSync(directory),
631
+ );
632
+ }
633
+ }
634
+ if (recoveryErrors.length > 0) {
635
+ throw new Error(recoveryErrors.join('; '));
636
+ }
637
+ }
638
+
639
+ function finalizeCanonicalUpdates(transaction) {
640
+ const warnings = [];
641
+ for (const item of transaction.applied ?? []) {
642
+ if (!item.hadOriginal) continue;
643
+ try {
644
+ rmSync(item.backupPath, { force: true });
645
+ } catch (error) {
646
+ warnings.push(
647
+ `Archived successfully but could not remove backup ${item.backupPath}: ${errorMessage(error)}`,
648
+ );
649
+ }
650
+ }
651
+ return warnings;
652
+ }
653
+
654
+ function archivedReport(report, options, changeName, updatedCapabilities) {
655
+ const sync =
656
+ updatedCapabilities.length === 0
657
+ ? '- None: no durable behavior delta.'
658
+ : `- Updated: ${updatedCapabilities.map((item) => `\`${item}\``).join(', ')}.`;
659
+ return report
660
+ .replace(/^\*\*Status\*\*:\s*READY\b/im, '**Status**: ARCHIVED')
661
+ .replaceAll(
662
+ 'openspec/changes/archive/YYYY-MM-DD-[feature]/',
663
+ `openspec/changes/archive/${options.date}-${changeName}/`,
664
+ )
665
+ .replace(
666
+ /^- Pending: archive applies declared durable deltas transactionally\.\s*$/im,
667
+ sync,
668
+ );
669
+ }
670
+
671
+ function recoverArchiveFailure(reportPath, originalReport, transaction) {
672
+ const recoveryErrors = [];
673
+ attemptRecovery(recoveryErrors, 'report recovery failed', () => {
674
+ injectFault('report-restore');
675
+ writeFileSync(reportPath, originalReport);
676
+ });
677
+ attemptRecovery(recoveryErrors, 'canonical recovery failed', () =>
678
+ rollbackCanonicalUpdates(transaction),
679
+ );
680
+ return recoveryErrors;
681
+ }
682
+
683
+ try {
684
+ const options = parseArgs(process.argv.slice(2));
685
+ const change = resolve(options.change);
686
+ const changesRoot = dirname(change);
687
+ if (basename(changesRoot) !== 'changes') {
688
+ throw new Error(
689
+ 'Change must be an immediate child of an openspec/changes directory',
690
+ );
691
+ }
692
+ if (!existsSync(join(change, 'spec.md'))) {
693
+ throw new Error('spec.md is required before archive');
694
+ }
695
+ const contract = parseChangeSpec(
696
+ readFileSync(join(change, 'spec.md'), 'utf8'),
697
+ );
698
+ const originalReport = assertArchivable(change, contract);
699
+
700
+ const archiveRoot = join(changesRoot, 'archive');
701
+ const changeName = basename(change);
702
+ const target = join(archiveRoot, `${options.date}-${changeName}`);
703
+ if (existsSync(target)) {
704
+ throw new Error(`Archive target already exists: ${target}`);
705
+ }
706
+
707
+ const specRoot = join(dirname(changesRoot), 'specs');
708
+ const updates = planCanonicalUpdates(specRoot, contract.deltas);
709
+ const updatedCapabilities = updates
710
+ .map((update) => update.capability)
711
+ .sort((left, right) => left.localeCompare(right));
712
+ const reportPath = join(change, 'archive-report.md');
713
+ const transaction = applyCanonicalUpdates(stageCanonicalUpdates(updates));
714
+
715
+ try {
716
+ writeFileSync(
717
+ reportPath,
718
+ archivedReport(originalReport, options, changeName, updatedCapabilities),
719
+ );
720
+ injectFault('after-report-write');
721
+ mkdirSync(archiveRoot, { recursive: true });
722
+ injectFault('before-change-move');
723
+ renameSync(change, target);
724
+ } catch (error) {
725
+ const recoveryErrors = recoverArchiveFailure(
726
+ reportPath,
727
+ originalReport,
728
+ transaction,
729
+ );
730
+ if (recoveryErrors.length > 0) {
731
+ const cause = errorMessage(error);
732
+ throw new Error(`${cause}; ${recoveryErrors.join('; ')}`);
733
+ }
734
+ throw error;
735
+ }
736
+ const warnings = finalizeCanonicalUpdates(transaction);
737
+
738
+ const result = {
739
+ status: 'archived',
740
+ archivePath: target,
741
+ specsUpdated: updatedCapabilities,
742
+ warnings,
743
+ };
744
+ process.stdout.write(`${options.json ? JSON.stringify(result) : target}\n`);
745
+ } catch (error) {
746
+ process.stderr.write(`${errorMessage(error)}\n`);
747
+ process.exitCode = 1;
748
+ }