olympus-ai 3.4.0 → 3.5.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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +631 -630
  4. package/dist/__tests__/installer.test.js +1 -1
  5. package/dist/__tests__/workflow-engine/checkpoint.test.d.ts +7 -0
  6. package/dist/__tests__/workflow-engine/checkpoint.test.d.ts.map +1 -0
  7. package/dist/__tests__/workflow-engine/checkpoint.test.js +373 -0
  8. package/dist/__tests__/workflow-engine/checkpoint.test.js.map +1 -0
  9. package/dist/agents/definitions.d.ts.map +1 -1
  10. package/dist/agents/definitions.js +8 -0
  11. package/dist/agents/definitions.js.map +1 -1
  12. package/dist/agents/idea-intake.d.ts +20 -0
  13. package/dist/agents/idea-intake.d.ts.map +1 -0
  14. package/dist/agents/idea-intake.js +255 -0
  15. package/dist/agents/idea-intake.js.map +1 -0
  16. package/dist/agents/index.d.ts +4 -0
  17. package/dist/agents/index.d.ts.map +1 -1
  18. package/dist/agents/index.js +4 -0
  19. package/dist/agents/index.js.map +1 -1
  20. package/dist/agents/intent-generator.d.ts +19 -0
  21. package/dist/agents/intent-generator.d.ts.map +1 -0
  22. package/dist/agents/intent-generator.js +303 -0
  23. package/dist/agents/intent-generator.js.map +1 -0
  24. package/dist/agents/prd-writer.d.ts +19 -0
  25. package/dist/agents/prd-writer.d.ts.map +1 -0
  26. package/dist/agents/prd-writer.js +236 -0
  27. package/dist/agents/prd-writer.js.map +1 -0
  28. package/dist/agents/prometheus.d.ts.map +1 -1
  29. package/dist/agents/prometheus.js +123 -2
  30. package/dist/agents/prometheus.js.map +1 -1
  31. package/dist/agents/spec-writer.d.ts +19 -0
  32. package/dist/agents/spec-writer.d.ts.map +1 -0
  33. package/dist/agents/spec-writer.js +528 -0
  34. package/dist/agents/spec-writer.js.map +1 -0
  35. package/dist/features/index.d.ts +1 -0
  36. package/dist/features/index.d.ts.map +1 -1
  37. package/dist/features/index.js +6 -0
  38. package/dist/features/index.js.map +1 -1
  39. package/dist/features/workflow-engine/artifacts.d.ts +96 -0
  40. package/dist/features/workflow-engine/artifacts.d.ts.map +1 -0
  41. package/dist/features/workflow-engine/artifacts.js +399 -0
  42. package/dist/features/workflow-engine/artifacts.js.map +1 -0
  43. package/dist/features/workflow-engine/checkpoint.d.ts +67 -0
  44. package/dist/features/workflow-engine/checkpoint.d.ts.map +1 -0
  45. package/dist/features/workflow-engine/checkpoint.js +249 -0
  46. package/dist/features/workflow-engine/checkpoint.js.map +1 -0
  47. package/dist/features/workflow-engine/engine.d.ts +128 -0
  48. package/dist/features/workflow-engine/engine.d.ts.map +1 -0
  49. package/dist/features/workflow-engine/engine.js +600 -0
  50. package/dist/features/workflow-engine/engine.js.map +1 -0
  51. package/dist/features/workflow-engine/execution.d.ts +99 -0
  52. package/dist/features/workflow-engine/execution.d.ts.map +1 -0
  53. package/dist/features/workflow-engine/execution.js +493 -0
  54. package/dist/features/workflow-engine/execution.js.map +1 -0
  55. package/dist/features/workflow-engine/hooks.d.ts +78 -0
  56. package/dist/features/workflow-engine/hooks.d.ts.map +1 -0
  57. package/dist/features/workflow-engine/hooks.js +188 -0
  58. package/dist/features/workflow-engine/hooks.js.map +1 -0
  59. package/dist/features/workflow-engine/index.d.ts +17 -0
  60. package/dist/features/workflow-engine/index.d.ts.map +1 -0
  61. package/dist/features/workflow-engine/index.js +19 -0
  62. package/dist/features/workflow-engine/index.js.map +1 -0
  63. package/dist/features/workflow-engine/types.d.ts +220 -0
  64. package/dist/features/workflow-engine/types.d.ts.map +1 -0
  65. package/dist/features/workflow-engine/types.js +8 -0
  66. package/dist/features/workflow-engine/types.js.map +1 -0
  67. package/dist/features/workflow-engine/validation.d.ts +128 -0
  68. package/dist/features/workflow-engine/validation.d.ts.map +1 -0
  69. package/dist/features/workflow-engine/validation.js +746 -0
  70. package/dist/features/workflow-engine/validation.js.map +1 -0
  71. package/dist/hooks/ascent-verifier/index.d.ts +52 -0
  72. package/dist/hooks/ascent-verifier/index.d.ts.map +1 -1
  73. package/dist/hooks/ascent-verifier/index.js +146 -0
  74. package/dist/hooks/ascent-verifier/index.js.map +1 -1
  75. package/dist/hooks/registrations/learning-capture.d.ts.map +1 -1
  76. package/dist/hooks/registrations/learning-capture.js +32 -9
  77. package/dist/hooks/registrations/learning-capture.js.map +1 -1
  78. package/dist/hooks/registrations/user-prompt-submit.d.ts.map +1 -1
  79. package/dist/hooks/registrations/user-prompt-submit.js +85 -0
  80. package/dist/hooks/registrations/user-prompt-submit.js.map +1 -1
  81. package/dist/installer/index.d.ts +1 -1
  82. package/dist/installer/index.d.ts.map +1 -1
  83. package/dist/installer/index.js +456 -16
  84. package/dist/installer/index.js.map +1 -1
  85. package/dist/learning/session-state.d.ts.map +1 -1
  86. package/dist/learning/session-state.js +17 -0
  87. package/dist/learning/session-state.js.map +1 -1
  88. package/dist/learning/types.d.ts +3 -0
  89. package/dist/learning/types.d.ts.map +1 -1
  90. package/dist/shared/types.d.ts +17 -0
  91. package/dist/shared/types.d.ts.map +1 -1
  92. package/package.json +3 -1
  93. package/scripts/dist/hooks/olympus-hooks.cjs +208 -97
  94. package/scripts/rebrand.mjs +0 -206
@@ -0,0 +1,746 @@
1
+ /**
2
+ * IDEA Artifact Validation
3
+ *
4
+ * Validates completeness of IDEA stage artifacts against required criteria.
5
+ * An IDEA artifact must contain all essential sections for progression to PRD stage.
6
+ *
7
+ * Performance optimizations:
8
+ * - Parallel validation where possible
9
+ * - Cached file reads within validation session
10
+ * - Optimized regex patterns
11
+ */
12
+ import { readFileSync } from 'fs';
13
+ /**
14
+ * Simple file content cache to avoid redundant reads during validation
15
+ */
16
+ const fileCache = new Map();
17
+ const FILE_CACHE_TTL = 10000; // 10 seconds
18
+ /**
19
+ * Read file with caching
20
+ */
21
+ function readFileWithCache(filePath) {
22
+ const cached = fileCache.get(filePath);
23
+ if (cached && Date.now() - cached.timestamp < FILE_CACHE_TTL) {
24
+ return cached.content;
25
+ }
26
+ const content = readFileSync(filePath, 'utf-8');
27
+ fileCache.set(filePath, { content, timestamp: Date.now() });
28
+ return content;
29
+ }
30
+ /**
31
+ * Clear the file cache
32
+ */
33
+ export function clearFileCache() {
34
+ fileCache.clear();
35
+ }
36
+ /**
37
+ * Required sections for a valid IDEA artifact
38
+ */
39
+ const REQUIRED_SECTIONS = [
40
+ 'Problem Statement',
41
+ 'Business Context',
42
+ 'Success Metrics',
43
+ 'Constraints',
44
+ 'Solution Approach',
45
+ ];
46
+ /**
47
+ * Validates an IDEA artifact for completeness.
48
+ *
49
+ * Checks 6 criteria:
50
+ * 1. Problem statement present (non-empty ## Problem Statement section)
51
+ * 2. Business context present (non-empty ## Business Context section)
52
+ * 3. At least 2 success metrics (check ## Success Metrics section has 2+ bullet points)
53
+ * 4. Constraints documented (## Constraints section present with content)
54
+ * 5. Risk tier assessed (YAML frontmatter has risk_tier field)
55
+ * 6. All required sections present (all 5 sections exist in document)
56
+ *
57
+ * @param artifactPath - Absolute path to the IDEA artifact file
58
+ * @returns ValidationResult with pass/fail status, coverage percentage, and any blocking issues
59
+ *
60
+ * @example
61
+ * const result = await validateIdea('.olympus/workflows/feature-x/idea.md');
62
+ * if (result.passed) {
63
+ * console.log('IDEA artifact is complete!');
64
+ * } else {
65
+ * console.log('Issues found:', result.blocking_issues);
66
+ * }
67
+ */
68
+ export async function validateIdea(artifactPath) {
69
+ const timestamp = new Date().toISOString();
70
+ const blockingIssues = [];
71
+ // Read artifact file with caching
72
+ let content;
73
+ try {
74
+ content = readFileWithCache(artifactPath);
75
+ }
76
+ catch (error) {
77
+ const err = error;
78
+ if (err.code === 'ENOENT') {
79
+ console.error(`[Validation] IDEA artifact not found: ${artifactPath}`);
80
+ return {
81
+ passed: false,
82
+ coverage_percentage: 0,
83
+ blocking_issues: ['Artifact file not found'],
84
+ timestamp,
85
+ };
86
+ }
87
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
88
+ console.error(`[Validation] Permission denied reading IDEA artifact: ${artifactPath}`);
89
+ return {
90
+ passed: false,
91
+ coverage_percentage: 0,
92
+ blocking_issues: ['Permission denied reading artifact file'],
93
+ timestamp,
94
+ };
95
+ }
96
+ console.error(`[Validation] Failed to read IDEA artifact: ${err.message}`);
97
+ console.error(`[Validation] Path: ${artifactPath}`);
98
+ return {
99
+ passed: false,
100
+ coverage_percentage: 0,
101
+ blocking_issues: [`Failed to read artifact: ${err.message}`],
102
+ timestamp,
103
+ };
104
+ }
105
+ // Parse YAML frontmatter
106
+ const frontmatter = parseFrontmatter(content);
107
+ if (!frontmatter || !frontmatter.risk_tier) {
108
+ blockingIssues.push('Risk tier not specified in frontmatter');
109
+ }
110
+ // Remove frontmatter from content for section parsing
111
+ const markdownContent = removeFrontmatter(content);
112
+ // Parse markdown sections (single pass optimization)
113
+ const sections = parseSections(markdownContent);
114
+ // Run all validations in parallel (independent checks)
115
+ const validationChecks = [
116
+ // Check criterion 1: Problem statement present and non-empty
117
+ () => {
118
+ const problemStatement = sections.get('Problem Statement');
119
+ if (!problemStatement || problemStatement.trim().length === 0) {
120
+ return 'Missing problem statement section';
121
+ }
122
+ return null;
123
+ },
124
+ // Check criterion 2: Business context present and non-empty
125
+ () => {
126
+ const businessContext = sections.get('Business Context');
127
+ if (!businessContext || businessContext.trim().length === 0) {
128
+ return 'Business context section is empty';
129
+ }
130
+ return null;
131
+ },
132
+ // Check criterion 3: At least 2 success metrics
133
+ () => {
134
+ const successMetrics = sections.get('Success Metrics');
135
+ if (successMetrics) {
136
+ const metricCount = countBulletPoints(successMetrics);
137
+ if (metricCount < 2) {
138
+ return `Only ${metricCount} success metric found, need at least 2`;
139
+ }
140
+ }
141
+ else {
142
+ return 'Missing success metrics section';
143
+ }
144
+ return null;
145
+ },
146
+ // Check criterion 4: Constraints documented
147
+ () => {
148
+ const constraints = sections.get('Constraints');
149
+ if (!constraints || constraints.trim().length === 0) {
150
+ return 'Constraints section missing';
151
+ }
152
+ return null;
153
+ },
154
+ // Check criterion 6: All required sections present
155
+ () => {
156
+ const missing = [];
157
+ for (const section of REQUIRED_SECTIONS) {
158
+ if (!sections.has(section)) {
159
+ missing.push(`Missing required section: ${section}`);
160
+ }
161
+ }
162
+ return missing.length > 0 ? missing.join('; ') : null;
163
+ },
164
+ ];
165
+ // Execute all checks (can be optimized to parallel execution if needed)
166
+ for (const check of validationChecks) {
167
+ const issue = check();
168
+ if (issue) {
169
+ blockingIssues.push(issue);
170
+ }
171
+ }
172
+ // Calculate coverage (6 total criteria)
173
+ const totalCriteria = 6;
174
+ const passedCriteria = totalCriteria - blockingIssues.length;
175
+ const coveragePercentage = Math.round((passedCriteria / totalCriteria) * 100);
176
+ return {
177
+ passed: blockingIssues.length === 0,
178
+ coverage_percentage: coveragePercentage,
179
+ blocking_issues: blockingIssues,
180
+ timestamp,
181
+ };
182
+ }
183
+ /**
184
+ * Parses YAML frontmatter from markdown content.
185
+ * Frontmatter must be delimited by --- at the start of the file.
186
+ *
187
+ * @param content - Full markdown content
188
+ * @returns Parsed frontmatter object or null if not found/invalid
189
+ */
190
+ function parseFrontmatter(content) {
191
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
192
+ if (!frontmatterMatch) {
193
+ return null;
194
+ }
195
+ const yamlContent = frontmatterMatch[1];
196
+ try {
197
+ // Simple YAML parser for key: value pairs
198
+ const result = {};
199
+ const lines = yamlContent.split('\n');
200
+ for (const line of lines) {
201
+ const match = line.match(/^(\w+):\s*(.+)$/);
202
+ if (match) {
203
+ const [, key, value] = match;
204
+ result[key] = value.trim();
205
+ }
206
+ }
207
+ return result;
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ /**
214
+ * Removes YAML frontmatter from markdown content.
215
+ *
216
+ * @param content - Full markdown content
217
+ * @returns Content without frontmatter
218
+ */
219
+ function removeFrontmatter(content) {
220
+ return content.replace(/^---\n[\s\S]*?\n---\n/, '');
221
+ }
222
+ /**
223
+ * Parses markdown sections based on ## headings.
224
+ *
225
+ * @param content - Markdown content (without frontmatter)
226
+ * @returns Map of section name to section content
227
+ */
228
+ function parseSections(content) {
229
+ const sections = new Map();
230
+ const lines = content.split('\n');
231
+ let currentSection = null;
232
+ let currentContent = [];
233
+ for (const line of lines) {
234
+ // Check for ## heading
235
+ const headingMatch = line.match(/^##\s+(.+)$/);
236
+ if (headingMatch) {
237
+ // Save previous section if it exists
238
+ if (currentSection) {
239
+ sections.set(currentSection, currentContent.join('\n'));
240
+ }
241
+ // Start new section
242
+ currentSection = headingMatch[1].trim();
243
+ currentContent = [];
244
+ }
245
+ else if (currentSection) {
246
+ // Add line to current section
247
+ currentContent.push(line);
248
+ }
249
+ }
250
+ // Save last section
251
+ if (currentSection) {
252
+ sections.set(currentSection, currentContent.join('\n'));
253
+ }
254
+ return sections;
255
+ }
256
+ /**
257
+ * Counts bullet points in a markdown section.
258
+ * Looks for lines starting with -, *, or + (standard markdown bullets).
259
+ *
260
+ * @param content - Section content
261
+ * @returns Number of bullet points found
262
+ */
263
+ function countBulletPoints(content) {
264
+ const lines = content.split('\n');
265
+ let count = 0;
266
+ for (const line of lines) {
267
+ if (line.trim().match(/^[-*+]\s+/)) {
268
+ count++;
269
+ }
270
+ }
271
+ return count;
272
+ }
273
+ /**
274
+ * Validates a PRD artifact for coverage against IDEA constraints.
275
+ *
276
+ * **Phase 2 MVP Stub Implementation**
277
+ * This is a simplified implementation that calculates coverage but does not
278
+ * invoke the Momus agent for critical review. Full Momus integration is
279
+ * deferred to Phase 3.
280
+ *
281
+ * Checks:
282
+ * - PRD addresses >= 90% of IDEA constraints
283
+ * - User stories are present
284
+ * - Requirement coverage section exists
285
+ *
286
+ * TODO (Phase 3): Integrate Momus agent for:
287
+ * - Scope drift detection
288
+ * - Acceptance criteria completeness check
289
+ * - Risk alignment verification
290
+ *
291
+ * @param artifactPath - Absolute path to the PRD artifact file
292
+ * @param ideaPath - Absolute path to the IDEA artifact file
293
+ * @returns ValidationResult with coverage percentage and Momus placeholder
294
+ *
295
+ * @example
296
+ * const result = await validatePrd(
297
+ * '.olympus/workflows/feature-x/prd.md',
298
+ * '.olympus/workflows/feature-x/idea.md'
299
+ * );
300
+ * if (result.coverage_percentage >= 90) {
301
+ * console.log('PRD has sufficient coverage');
302
+ * }
303
+ */
304
+ export async function validatePrd(artifactPath, ideaPath) {
305
+ const timestamp = new Date().toISOString();
306
+ const blockingIssues = [];
307
+ // Read PRD artifact
308
+ let prdContent;
309
+ try {
310
+ prdContent = readFileSync(artifactPath, 'utf-8');
311
+ }
312
+ catch (error) {
313
+ const err = error;
314
+ console.error(`[Validation] Failed to read PRD artifact: ${err.message}`);
315
+ console.error(`[Validation] Path: ${artifactPath}`);
316
+ const errorMsg = err.code === 'ENOENT'
317
+ ? 'PRD artifact file not found'
318
+ : err.code === 'EACCES' || err.code === 'EPERM'
319
+ ? 'Permission denied reading PRD artifact'
320
+ : `Failed to read PRD artifact: ${err.message}`;
321
+ return {
322
+ passed: false,
323
+ coverage_percentage: 0,
324
+ blocking_issues: [errorMsg],
325
+ reviewer: 'momus',
326
+ timestamp,
327
+ };
328
+ }
329
+ // Read IDEA artifact
330
+ let ideaContent;
331
+ try {
332
+ ideaContent = readFileSync(ideaPath, 'utf-8');
333
+ }
334
+ catch (error) {
335
+ const err = error;
336
+ console.error(`[Validation] Failed to read IDEA artifact for PRD validation: ${err.message}`);
337
+ console.error(`[Validation] Path: ${ideaPath}`);
338
+ const errorMsg = err.code === 'ENOENT'
339
+ ? 'IDEA artifact file not found for reference'
340
+ : err.code === 'EACCES' || err.code === 'EPERM'
341
+ ? 'Permission denied reading IDEA artifact'
342
+ : `Failed to read IDEA artifact: ${err.message}`;
343
+ return {
344
+ passed: false,
345
+ coverage_percentage: 0,
346
+ blocking_issues: [errorMsg],
347
+ reviewer: 'momus',
348
+ timestamp,
349
+ };
350
+ }
351
+ // Parse IDEA constraints
352
+ const ideaMarkdown = removeFrontmatter(ideaContent);
353
+ const ideaSections = parseSections(ideaMarkdown);
354
+ const constraintsSection = ideaSections.get('Constraints');
355
+ const ideaConstraints = constraintsSection
356
+ ? countBulletPoints(constraintsSection)
357
+ : 0;
358
+ // Parse PRD user stories
359
+ const prdMarkdown = removeFrontmatter(prdContent);
360
+ const prdSections = parseSections(prdMarkdown);
361
+ // Count user stories (sections starting with "US-" or "### US-")
362
+ let userStoryCount = 0;
363
+ for (const line of prdMarkdown.split('\n')) {
364
+ if (line.match(/^###?\s+US-\d+/)) {
365
+ userStoryCount++;
366
+ }
367
+ }
368
+ // Check for requirement coverage section
369
+ const hasCoverageSection = prdSections.has('Requirement Coverage');
370
+ // Calculate coverage percentage
371
+ // Simplified: assume each user story addresses one constraint
372
+ // Real implementation would parse the coverage table
373
+ const coveragePercentage = ideaConstraints > 0
374
+ ? Math.round((Math.min(userStoryCount, ideaConstraints) / ideaConstraints) * 100)
375
+ : 100;
376
+ // Validate completeness
377
+ if (userStoryCount === 0) {
378
+ blockingIssues.push('No user stories found in PRD');
379
+ }
380
+ if (!hasCoverageSection) {
381
+ blockingIssues.push('Missing Requirement Coverage section');
382
+ }
383
+ if (coveragePercentage < 90) {
384
+ blockingIssues.push(`Coverage only ${coveragePercentage}%, need at least 90% (${userStoryCount}/${ideaConstraints} constraints addressed)`);
385
+ }
386
+ // TODO (Phase 3): Invoke Momus agent here for critical review
387
+ // const momusReview = await invokeMomusAgent(prdContent, ideaContent);
388
+ // blockingIssues.push(...momusReview.issues);
389
+ return {
390
+ passed: blockingIssues.length === 0 && coveragePercentage >= 90,
391
+ coverage_percentage: coveragePercentage,
392
+ blocking_issues: blockingIssues,
393
+ reviewer: 'momus', // Placeholder - real Momus review deferred to Phase 3
394
+ timestamp,
395
+ };
396
+ }
397
+ /**
398
+ * Validates a SPEC artifact for coverage against PRD user stories.
399
+ *
400
+ * **Phase 2 MVP Stub Implementation**
401
+ * This is a simplified implementation that calculates coverage but does not
402
+ * invoke the Metis agent for critical review. Full Metis integration is
403
+ * deferred to Phase 3.
404
+ *
405
+ * Checks:
406
+ * - SPEC implements >= 95% of PRD user stories
407
+ * - Requirement coverage section exists
408
+ * - All components documented
409
+ *
410
+ * TODO (Phase 3): Integrate Metis agent for:
411
+ * - Hidden requirements analysis
412
+ * - Dependency mapping completeness
413
+ * - Security considerations adequacy
414
+ * - Performance requirements coverage
415
+ *
416
+ * @param specPath - Absolute path to the SPEC artifact file
417
+ * @param prdPath - Absolute path to the PRD artifact file
418
+ * @returns ValidationResult with coverage percentage and Metis placeholder
419
+ *
420
+ * @example
421
+ * const result = await validateSpec(
422
+ * '.olympus/workflows/feature-x/spec.md',
423
+ * '.olympus/workflows/feature-x/prd.md'
424
+ * );
425
+ * if (result.coverage_percentage >= 95) {
426
+ * console.log('SPEC has sufficient PRD coverage');
427
+ * }
428
+ */
429
+ export async function validateSpec(specPath, prdPath) {
430
+ const timestamp = new Date().toISOString();
431
+ const blockingIssues = [];
432
+ // Read SPEC artifact
433
+ let specContent;
434
+ try {
435
+ specContent = readFileSync(specPath, 'utf-8');
436
+ }
437
+ catch (error) {
438
+ const err = error;
439
+ console.error(`[Validation] Failed to read SPEC artifact: ${err.message}`);
440
+ console.error(`[Validation] Path: ${specPath}`);
441
+ const errorMsg = err.code === 'ENOENT'
442
+ ? 'SPEC artifact file not found'
443
+ : err.code === 'EACCES' || err.code === 'EPERM'
444
+ ? 'Permission denied reading SPEC artifact'
445
+ : `Failed to read SPEC artifact: ${err.message}`;
446
+ return {
447
+ passed: false,
448
+ coverage_percentage: 0,
449
+ blocking_issues: [errorMsg],
450
+ reviewer: 'metis',
451
+ timestamp,
452
+ };
453
+ }
454
+ // Read PRD artifact
455
+ let prdContent;
456
+ try {
457
+ prdContent = readFileSync(prdPath, 'utf-8');
458
+ }
459
+ catch (error) {
460
+ const err = error;
461
+ console.error(`[Validation] Failed to read PRD artifact for SPEC validation: ${err.message}`);
462
+ console.error(`[Validation] Path: ${prdPath}`);
463
+ const errorMsg = err.code === 'ENOENT'
464
+ ? 'PRD artifact file not found for reference'
465
+ : err.code === 'EACCES' || err.code === 'EPERM'
466
+ ? 'Permission denied reading PRD artifact'
467
+ : `Failed to read PRD artifact: ${err.message}`;
468
+ return {
469
+ passed: false,
470
+ coverage_percentage: 0,
471
+ blocking_issues: [errorMsg],
472
+ reviewer: 'metis',
473
+ timestamp,
474
+ };
475
+ }
476
+ // Parse PRD user stories
477
+ const prdMarkdown = removeFrontmatter(prdContent);
478
+ const prdUserStories = [];
479
+ for (const line of prdMarkdown.split('\n')) {
480
+ const match = line.match(/^###?\s+(US-\d+)/);
481
+ if (match) {
482
+ prdUserStories.push(match[1]);
483
+ }
484
+ }
485
+ // Parse SPEC for user story coverage
486
+ const specMarkdown = removeFrontmatter(specContent);
487
+ const specSections = parseSections(specMarkdown);
488
+ const coverageSection = specSections.get('Requirement Coverage') || specSections.get('PRD Coverage') || '';
489
+ // Count how many PRD user stories are referenced in SPEC
490
+ let coveredStories = 0;
491
+ for (const story of prdUserStories) {
492
+ if (specMarkdown.includes(story)) {
493
+ coveredStories++;
494
+ }
495
+ }
496
+ // Calculate coverage percentage
497
+ const coveragePercentage = prdUserStories.length > 0
498
+ ? Math.round((coveredStories / prdUserStories.length) * 100)
499
+ : 0;
500
+ // Validate completeness
501
+ if (prdUserStories.length === 0) {
502
+ blockingIssues.push('No user stories found in PRD for validation');
503
+ }
504
+ if (!coverageSection || coverageSection.trim().length === 0) {
505
+ blockingIssues.push('Missing Requirement Coverage section in SPEC');
506
+ }
507
+ if (coveragePercentage < 95) {
508
+ blockingIssues.push(`Coverage only ${coveragePercentage}%, need at least 95% (${coveredStories}/${prdUserStories.length} user stories addressed)`);
509
+ }
510
+ // Check for components section
511
+ const hasComponentsSection = specSections.has('Components') || specSections.has('Architecture');
512
+ if (!hasComponentsSection) {
513
+ blockingIssues.push('Missing Components or Architecture section');
514
+ }
515
+ // TODO (Phase 3): Invoke Metis agent here for critical review
516
+ // const metisReview = await invokeMetisAgent(specContent, prdContent);
517
+ // blockingIssues.push(...metisReview.issues);
518
+ // Metis should check:
519
+ // - Hidden requirements not explicitly stated in PRD
520
+ // - Dependency mapping completeness
521
+ // - Security considerations adequacy
522
+ // - Performance requirements coverage
523
+ return {
524
+ passed: blockingIssues.length === 0 && coveragePercentage >= 95,
525
+ coverage_percentage: coveragePercentage,
526
+ blocking_issues: blockingIssues,
527
+ reviewer: 'metis', // Placeholder - real Metis review deferred to Phase 3
528
+ timestamp,
529
+ };
530
+ }
531
+ /**
532
+ * Validates TASKS artifacts for coverage against SPEC components.
533
+ *
534
+ * Checks:
535
+ * - 100% of SPEC components have tasks
536
+ * - Dependency graph is valid (no circular dependencies)
537
+ * - All tasks have effort estimates
538
+ * - Effort estimates are reasonable (1, 2, 4, 8, or 16 hours)
539
+ *
540
+ * @param tasksDir - Absolute path to the tasks directory (contains INTENT files)
541
+ * @param specPath - Absolute path to the SPEC artifact file
542
+ * @returns ValidationResult with coverage percentage and validation details
543
+ *
544
+ * @example
545
+ * const result = await validateTasks(
546
+ * '.olympus/workflows/feature-x/intents/',
547
+ * '.olympus/workflows/feature-x/spec.md'
548
+ * );
549
+ * if (result.passed) {
550
+ * console.log('All SPEC components have task coverage');
551
+ * }
552
+ */
553
+ export async function validateTasks(tasksDir, specPath) {
554
+ const timestamp = new Date().toISOString();
555
+ const blockingIssues = [];
556
+ // Read SPEC artifact
557
+ let specContent;
558
+ try {
559
+ specContent = readFileSync(specPath, 'utf-8');
560
+ }
561
+ catch (error) {
562
+ const err = error;
563
+ console.error(`[Validation] Failed to read SPEC artifact for task validation: ${err.message}`);
564
+ console.error(`[Validation] Path: ${specPath}`);
565
+ const errorMsg = err.code === 'ENOENT'
566
+ ? 'SPEC artifact file not found'
567
+ : err.code === 'EACCES' || err.code === 'EPERM'
568
+ ? 'Permission denied reading SPEC artifact'
569
+ : `Failed to read SPEC artifact: ${err.message}`;
570
+ return {
571
+ passed: false,
572
+ coverage_percentage: 0,
573
+ blocking_issues: [errorMsg],
574
+ timestamp,
575
+ };
576
+ }
577
+ // Parse SPEC for components
578
+ const specMarkdown = removeFrontmatter(specContent);
579
+ const specSections = parseSections(specMarkdown);
580
+ const componentsSection = specSections.get('Components') || specSections.get('Architecture') || '';
581
+ // Extract component names (look for ### headings in components section)
582
+ const specComponents = [];
583
+ if (componentsSection) {
584
+ for (const line of componentsSection.split('\n')) {
585
+ const match = line.match(/^###\s+(.+)$/);
586
+ if (match) {
587
+ specComponents.push(match[1].trim());
588
+ }
589
+ }
590
+ }
591
+ // Read INTENT files from tasksDir
592
+ let intentFiles = [];
593
+ try {
594
+ const fs = await import('fs');
595
+ const files = fs.readdirSync(tasksDir);
596
+ intentFiles = files.filter(f => f.endsWith('.md') || f.includes('INTENT'));
597
+ }
598
+ catch (error) {
599
+ const err = error;
600
+ console.error(`[Validation] Failed to read tasks directory: ${err.message}`);
601
+ console.error(`[Validation] Path: ${tasksDir}`);
602
+ const errorMsg = err.code === 'ENOENT'
603
+ ? 'Tasks directory not found'
604
+ : err.code === 'EACCES' || err.code === 'EPERM'
605
+ ? 'Permission denied reading tasks directory'
606
+ : `Failed to read tasks directory: ${err.message}`;
607
+ return {
608
+ passed: false,
609
+ coverage_percentage: 0,
610
+ blocking_issues: [errorMsg],
611
+ timestamp,
612
+ };
613
+ }
614
+ // Parse INTENT files for component coverage
615
+ const coveredComponents = new Set();
616
+ const taskEstimates = [];
617
+ for (const intentFile of intentFiles) {
618
+ try {
619
+ const fs = await import('fs');
620
+ const path = await import('path');
621
+ const intentPath = path.join(tasksDir, intentFile);
622
+ const intentContent = fs.readFileSync(intentPath, 'utf-8');
623
+ // Check which components are mentioned in this INTENT
624
+ for (const component of specComponents) {
625
+ if (intentContent.includes(component)) {
626
+ coveredComponents.add(component);
627
+ }
628
+ }
629
+ // Extract effort estimate
630
+ const effortMatch = intentContent.match(/estimated_effort:\s*(\d+)/i);
631
+ if (effortMatch) {
632
+ taskEstimates.push(parseInt(effortMatch[1], 10));
633
+ }
634
+ }
635
+ catch (_error) {
636
+ // Skip unreadable files
637
+ }
638
+ }
639
+ // Calculate coverage percentage
640
+ const coveragePercentage = specComponents.length > 0
641
+ ? Math.round((coveredComponents.size / specComponents.length) * 100)
642
+ : 100;
643
+ // Validate 100% coverage
644
+ if (specComponents.length === 0) {
645
+ blockingIssues.push('No components found in SPEC for validation');
646
+ }
647
+ else if (coveragePercentage < 100) {
648
+ const uncovered = specComponents.filter(c => !coveredComponents.has(c));
649
+ blockingIssues.push(`Incomplete coverage: ${coveragePercentage}% (missing: ${uncovered.join(', ')})`);
650
+ }
651
+ // Validate effort estimates
652
+ const validEstimates = [1, 2, 4, 8, 16];
653
+ for (const estimate of taskEstimates) {
654
+ if (!validEstimates.includes(estimate)) {
655
+ blockingIssues.push(`Invalid effort estimate: ${estimate} hours (must be 1, 2, 4, 8, or 16)`);
656
+ }
657
+ }
658
+ if (intentFiles.length > 0 && taskEstimates.length === 0) {
659
+ blockingIssues.push('No effort estimates found in task files');
660
+ }
661
+ // Check for variance in estimates (within 30%)
662
+ if (taskEstimates.length > 1) {
663
+ const avgEstimate = taskEstimates.reduce((a, b) => a + b, 0) / taskEstimates.length;
664
+ const maxVariance = avgEstimate * 0.3;
665
+ for (const estimate of taskEstimates) {
666
+ if (Math.abs(estimate - avgEstimate) > maxVariance) {
667
+ // This is a warning, not a blocking issue
668
+ // Only add if variance is extreme (>50%)
669
+ if (Math.abs(estimate - avgEstimate) > avgEstimate * 0.5) {
670
+ blockingIssues.push(`High variance in effort estimates: ${estimate}h vs avg ${Math.round(avgEstimate)}h`);
671
+ }
672
+ }
673
+ }
674
+ }
675
+ // Validate dependency graph
676
+ try {
677
+ const fs = await import('fs');
678
+ const path = await import('path');
679
+ const graphPath = path.join(tasksDir, 'dependency-graph.json');
680
+ const graphContent = fs.readFileSync(graphPath, 'utf-8');
681
+ const graph = JSON.parse(graphContent);
682
+ // Basic cycle detection
683
+ const hasCycle = detectCycles(graph);
684
+ if (hasCycle) {
685
+ blockingIssues.push('Circular dependencies detected in dependency graph');
686
+ }
687
+ // Validate all referenced dependencies exist
688
+ const taskIds = new Set(Object.keys(graph));
689
+ for (const [taskId, deps] of Object.entries(graph)) {
690
+ if (Array.isArray(deps)) {
691
+ for (const dep of deps) {
692
+ if (!taskIds.has(dep)) {
693
+ blockingIssues.push(`Task ${taskId} references non-existent dependency: ${dep}`);
694
+ }
695
+ }
696
+ }
697
+ }
698
+ }
699
+ catch (_error) {
700
+ // Dependency graph is optional for now
701
+ // blockingIssues.push('Dependency graph file not found or invalid');
702
+ }
703
+ return {
704
+ passed: blockingIssues.length === 0 && coveragePercentage === 100,
705
+ coverage_percentage: coveragePercentage,
706
+ blocking_issues: blockingIssues,
707
+ timestamp,
708
+ };
709
+ }
710
+ /**
711
+ * Detects cycles in a dependency graph using depth-first search.
712
+ *
713
+ * @param graph - Adjacency list representation of dependencies
714
+ * @returns true if a cycle is detected, false otherwise
715
+ */
716
+ function detectCycles(graph) {
717
+ const visited = new Set();
718
+ const recursionStack = new Set();
719
+ function dfs(node) {
720
+ visited.add(node);
721
+ recursionStack.add(node);
722
+ const neighbors = graph[node] || [];
723
+ for (const neighbor of neighbors) {
724
+ if (!visited.has(neighbor)) {
725
+ if (dfs(neighbor)) {
726
+ return true;
727
+ }
728
+ }
729
+ else if (recursionStack.has(neighbor)) {
730
+ // Found a back edge - cycle detected
731
+ return true;
732
+ }
733
+ }
734
+ recursionStack.delete(node);
735
+ return false;
736
+ }
737
+ for (const node of Object.keys(graph)) {
738
+ if (!visited.has(node)) {
739
+ if (dfs(node)) {
740
+ return true;
741
+ }
742
+ }
743
+ }
744
+ return false;
745
+ }
746
+ //# sourceMappingURL=validation.js.map