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