neuro-cli 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1485 @@
1
+ // ============================================================
2
+ // NeuroCLI - Spec-Driven Development Pipeline (GAP-30)
3
+ // Inspired by Kiro/AWS spec-driven development approach
4
+ // Instead of going directly from prompt to code, generate
5
+ // structured specifications first, then implement from specs.
6
+ // Pipeline: Requirements -> Design -> Plan -> Implement -> Verify
7
+ // ============================================================
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync, } from 'fs';
9
+ import { join, resolve } from 'path';
10
+ import { randomUUID } from 'crypto';
11
+ import { createHash } from 'crypto';
12
+ import { execSync } from 'child_process';
13
+ // ============================================================
14
+ // Constants
15
+ // ============================================================
16
+ const SPECS_DIR = '.neuro/specs';
17
+ const SPEC_FILE_EXTENSION = '.md';
18
+ const REQUIREMENTS_SYSTEM_PROMPT = `You are a requirements analyst for a software project. Given a natural language description, generate structured functional requirements.
19
+
20
+ For each requirement, provide:
21
+ - A clear title
22
+ - A detailed description
23
+ - A priority (must, should, or could) using the MoSCoW method
24
+ - Testable acceptance criteria
25
+
26
+ Output your response as a JSON array of requirement objects with this exact shape:
27
+ [
28
+ {
29
+ "id": "FR-001",
30
+ "title": "...",
31
+ "description": "...",
32
+ "priority": "must|should|could",
33
+ "acceptanceCriteria": ["...", "..."],
34
+ "status": "pending"
35
+ }
36
+ ]
37
+
38
+ Be thorough, precise, and ensure every acceptance criterion is independently testable. Number requirements sequentially starting from FR-001.`;
39
+ const DESIGN_SYSTEM_PROMPT = `You are a software architect. Given a set of functional requirements, generate a technical design document.
40
+
41
+ Provide:
42
+ - Architecture overview: High-level architecture pattern and key decisions
43
+ - Components: List of components with name, description, responsibilities, interfaces, and dependencies
44
+ - Data flow: How data moves through the system
45
+ - API design: Key APIs or interfaces (as string descriptions)
46
+ - Error handling: Strategy for error handling and edge cases
47
+
48
+ Output your response as a JSON object with this exact shape:
49
+ {
50
+ "architecture": "...",
51
+ "components": [
52
+ {
53
+ "name": "...",
54
+ "description": "...",
55
+ "responsibilities": ["..."],
56
+ "interfaces": ["..."],
57
+ "dependencies": ["..."]
58
+ }
59
+ ],
60
+ "dataFlow": "...",
61
+ "apiDesign": ["..."],
62
+ "errorHandling": "..."
63
+ }
64
+
65
+ Be specific about component interactions and data flow. Each component should have clear, single responsibilities.`;
66
+ const PLAN_SYSTEM_PROMPT = `You are a project planner. Given a technical design document, generate a step-by-step implementation plan.
67
+
68
+ Provide:
69
+ - Phases: Ordered groups of tasks, each with a name and order number
70
+ - Tasks within each phase: Specific, actionable implementation tasks with optional file paths and verification steps
71
+ - Estimated effort: Overall effort estimate
72
+ - Dependencies: External dependencies or prerequisites
73
+ - Risks: Potential risks with likelihood, impact, and mitigation strategies
74
+
75
+ Output your response as a JSON object with this exact shape:
76
+ {
77
+ "phases": [
78
+ {
79
+ "name": "Phase 1: ...",
80
+ "tasks": [
81
+ {
82
+ "id": "T-001",
83
+ "description": "...",
84
+ "completed": false,
85
+ "files": ["path/to/file.ts"],
86
+ "verification": "..."
87
+ }
88
+ ],
89
+ "order": 1
90
+ }
91
+ ],
92
+ "estimatedEffort": "...",
93
+ "dependencies": ["..."],
94
+ "risks": [
95
+ {
96
+ "id": "R-001",
97
+ "description": "...",
98
+ "likelihood": "low|medium|high",
99
+ "impact": "low|medium|high",
100
+ "mitigation": "..."
101
+ }
102
+ ]
103
+ }
104
+
105
+ Make tasks small, incremental, and independently verifiable. Each phase should produce a working checkpoint.`;
106
+ const VERIFICATION_SYSTEM_PROMPT = `You are a QA engineer. Given a specification with requirements and acceptance criteria, verify the implementation.
107
+
108
+ For each acceptance criterion, determine:
109
+ - Whether it is met (passed/failed)
110
+ - Evidence supporting the determination
111
+
112
+ Also provide:
113
+ - An overall score (0-100)
114
+ - A list of issues found
115
+ - Suggestions for improvement
116
+
117
+ Output your response as a JSON object with this exact shape:
118
+ {
119
+ "passed": true|false,
120
+ "criteriaResults": [
121
+ {
122
+ "requirementId": "FR-001",
123
+ "criteriaIndex": 0,
124
+ "criteriaText": "...",
125
+ "passed": true|false,
126
+ "evidence": "..."
127
+ }
128
+ ],
129
+ "overallScore": 85,
130
+ "issues": ["..."],
131
+ "suggestions": ["..."]
132
+ }
133
+
134
+ Be rigorous and evidence-based. A criterion passes only when you can confirm it from the code or tests.`;
135
+ const IMPLEMENTATION_TASK_PROMPT = `You are an expert software developer. Implement the following task as part of a larger project.
136
+
137
+ Task: {taskDescription}
138
+
139
+ Files to modify: {files}
140
+ Verification criteria: {verification}
141
+
142
+ Context from design:
143
+ {designContext}
144
+
145
+ Requirements this task addresses:
146
+ {relevantRequirements}
147
+
148
+ Write the implementation code. Create or modify the specified files. Ensure the code is complete, well-structured, and follows the project's existing patterns. After implementation, describe what you did and verify it meets the acceptance criteria.`;
149
+ // ============================================================
150
+ // SpecDrivenPipeline class
151
+ // ============================================================
152
+ export class SpecDrivenPipeline {
153
+ engine;
154
+ projectRoot;
155
+ specsDir;
156
+ model;
157
+ totalCost;
158
+ constructor(engine, projectRoot, model) {
159
+ this.engine = engine;
160
+ this.projectRoot = resolve(projectRoot);
161
+ this.specsDir = join(this.projectRoot, SPECS_DIR);
162
+ this.model = model || 'qwen/qwen3-coder:free';
163
+ this.totalCost = 0;
164
+ this.ensureSpecsDir();
165
+ }
166
+ // -------------------------------------------------------------------
167
+ // Directory management
168
+ // -------------------------------------------------------------------
169
+ ensureSpecsDir() {
170
+ if (!existsSync(this.specsDir)) {
171
+ mkdirSync(this.specsDir, { recursive: true });
172
+ }
173
+ }
174
+ // -------------------------------------------------------------------
175
+ // Hashing utilities
176
+ // -------------------------------------------------------------------
177
+ hashContent(content) {
178
+ return createHash('sha256').update(content).digest('hex');
179
+ }
180
+ // -------------------------------------------------------------------
181
+ // LLM helper
182
+ // -------------------------------------------------------------------
183
+ async callLLM(systemPrompt, userPrompt) {
184
+ const fullPrompt = `${systemPrompt}\n\n---\n\n${userPrompt}`;
185
+ const result = await this.engine.runPrompt(fullPrompt, this.model);
186
+ this.totalCost += result.cost;
187
+ if (result.error) {
188
+ throw new Error(`LLM call failed: ${result.error}`);
189
+ }
190
+ return result.text;
191
+ }
192
+ // -------------------------------------------------------------------
193
+ // JSON extraction from LLM responses
194
+ // -------------------------------------------------------------------
195
+ extractJSON(text) {
196
+ // Try to find a JSON block wrapped in markdown code fences
197
+ const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
198
+ if (codeBlockMatch) {
199
+ return codeBlockMatch[1].trim();
200
+ }
201
+ // Try to find a raw JSON array or object
202
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
203
+ const objectMatch = text.match(/\{[\s\S]*\}/);
204
+ if (arrayMatch && objectMatch) {
205
+ // Return whichever comes first
206
+ const arrayIndex = text.indexOf(arrayMatch[0]);
207
+ const objectIndex = text.indexOf(objectMatch[0]);
208
+ return arrayIndex < objectIndex ? arrayMatch[0] : objectMatch[0];
209
+ }
210
+ if (arrayMatch)
211
+ return arrayMatch[0];
212
+ if (objectMatch)
213
+ return objectMatch[0];
214
+ return text.trim();
215
+ }
216
+ parseJSON(text) {
217
+ const jsonStr = this.extractJSON(text);
218
+ try {
219
+ return JSON.parse(jsonStr);
220
+ }
221
+ catch {
222
+ throw new Error(`Failed to parse LLM response as JSON: ${jsonStr.substring(0, 200)}...`);
223
+ }
224
+ }
225
+ // -------------------------------------------------------------------
226
+ // Pipeline Stage 1: Generate Requirements
227
+ // -------------------------------------------------------------------
228
+ async generateRequirements(prompt) {
229
+ const userPrompt = `Generate detailed functional requirements for the following feature request:\n\n${prompt}`;
230
+ const response = await this.callLLM(REQUIREMENTS_SYSTEM_PROMPT, userPrompt);
231
+ const requirements = this.parseJSON(response);
232
+ // Validate and normalize
233
+ return requirements.map((req, index) => ({
234
+ id: req.id || `FR-${String(index + 1).padStart(3, '0')}`,
235
+ title: req.title || `Requirement ${index + 1}`,
236
+ description: req.description || '',
237
+ priority: ['must', 'should', 'could'].includes(req.priority) ? req.priority : 'should',
238
+ acceptanceCriteria: Array.isArray(req.acceptanceCriteria) ? req.acceptanceCriteria : [],
239
+ status: 'pending',
240
+ }));
241
+ }
242
+ // -------------------------------------------------------------------
243
+ // Pipeline Stage 2: Generate Design
244
+ // -------------------------------------------------------------------
245
+ async generateDesign(requirements) {
246
+ const requirementsText = requirements
247
+ .map(r => `${r.id}: ${r.title}\n Priority: ${r.priority}\n Description: ${r.description}\n Acceptance Criteria:\n${r.acceptanceCriteria.map(ac => ` - ${ac}`).join('\n')}`)
248
+ .join('\n\n');
249
+ const userPrompt = `Generate a technical design document based on these requirements:\n\n${requirementsText}`;
250
+ const response = await this.callLLM(DESIGN_SYSTEM_PROMPT, userPrompt);
251
+ const design = this.parseJSON(response);
252
+ return {
253
+ architecture: design.architecture || '',
254
+ components: Array.isArray(design.components)
255
+ ? design.components.map((c, i) => ({
256
+ name: c.name || `Component ${i + 1}`,
257
+ description: c.description || '',
258
+ responsibilities: Array.isArray(c.responsibilities) ? c.responsibilities : [],
259
+ interfaces: Array.isArray(c.interfaces) ? c.interfaces : [],
260
+ dependencies: Array.isArray(c.dependencies) ? c.dependencies : [],
261
+ }))
262
+ : [],
263
+ dataFlow: design.dataFlow || '',
264
+ apiDesign: Array.isArray(design.apiDesign) ? design.apiDesign : [],
265
+ errorHandling: design.errorHandling || '',
266
+ };
267
+ }
268
+ // -------------------------------------------------------------------
269
+ // Pipeline Stage 3: Generate Implementation Plan
270
+ // -------------------------------------------------------------------
271
+ async generatePlan(design) {
272
+ const designText = [
273
+ `Architecture: ${design.architecture}`,
274
+ `Data Flow: ${design.dataFlow}`,
275
+ `Error Handling: ${design.errorHandling}`,
276
+ `Components:\n${design.components.map(c => ` - ${c.name}: ${c.description}\n Responsibilities: ${c.responsibilities.join(', ')}\n Interfaces: ${c.interfaces.join(', ')}\n Dependencies: ${c.dependencies.join(', ')}`).join('\n')}`,
277
+ `API Design:\n${design.apiDesign.map(a => ` - ${a}`).join('\n')}`,
278
+ ].join('\n\n');
279
+ const userPrompt = `Generate a step-by-step implementation plan for this design:\n\n${designText}`;
280
+ const response = await this.callLLM(PLAN_SYSTEM_PROMPT, userPrompt);
281
+ const plan = this.parseJSON(response);
282
+ return {
283
+ phases: Array.isArray(plan.phases)
284
+ ? plan.phases.map((phase, pi) => ({
285
+ name: phase.name || `Phase ${pi + 1}`,
286
+ order: phase.order ?? pi + 1,
287
+ tasks: Array.isArray(phase.tasks)
288
+ ? phase.tasks.map((task, ti) => ({
289
+ id: task.id || `T-${String(pi + 1).padStart(1, '0')}-${String(ti + 1).padStart(2, '0')}`,
290
+ description: task.description || '',
291
+ completed: false,
292
+ files: Array.isArray(task.files) ? task.files : undefined,
293
+ verification: task.verification || undefined,
294
+ }))
295
+ : [],
296
+ }))
297
+ : [],
298
+ estimatedEffort: plan.estimatedEffort || 'Unknown',
299
+ dependencies: Array.isArray(plan.dependencies) ? plan.dependencies : [],
300
+ risks: Array.isArray(plan.risks)
301
+ ? plan.risks.map((risk, ri) => ({
302
+ id: risk.id || `R-${String(ri + 1).padStart(3, '0')}`,
303
+ description: risk.description || '',
304
+ likelihood: ['low', 'medium', 'high'].includes(risk.likelihood) ? risk.likelihood : 'medium',
305
+ impact: ['low', 'medium', 'high'].includes(risk.impact) ? risk.impact : 'medium',
306
+ mitigation: risk.mitigation || '',
307
+ }))
308
+ : [],
309
+ };
310
+ }
311
+ // -------------------------------------------------------------------
312
+ // Pipeline Stage 4: Execute Implementation Plan
313
+ // -------------------------------------------------------------------
314
+ async executePlan(plan, options) {
315
+ const startTime = Date.now();
316
+ const totalTasks = plan.phases.reduce((sum, p) => sum + p.tasks.length, 0);
317
+ const result = {
318
+ success: true,
319
+ phasesCompleted: 0,
320
+ tasksCompleted: 0,
321
+ tasksTotal: totalTasks,
322
+ errors: [],
323
+ filesModified: [],
324
+ durationMs: 0,
325
+ };
326
+ const maxIterations = options?.maxIterations ?? 3;
327
+ const phasesToRun = options?.phases ?? plan.phases.map((_, i) => i);
328
+ const resume = options?.resume ?? false;
329
+ for (const phaseIndex of phasesToRun) {
330
+ if (phaseIndex >= plan.phases.length)
331
+ continue;
332
+ const phase = plan.phases[phaseIndex];
333
+ let allTasksComplete = true;
334
+ for (let taskIndex = 0; taskIndex < phase.tasks.length; taskIndex++) {
335
+ const task = phase.tasks[taskIndex];
336
+ // Skip already completed tasks when resuming
337
+ if (resume && task.completed) {
338
+ result.tasksCompleted++;
339
+ continue;
340
+ }
341
+ options?.onProgress?.(phase.name, task.description, 0);
342
+ const taskPrompt = IMPLEMENTATION_TASK_PROMPT
343
+ .replace('{taskDescription}', task.description)
344
+ .replace('{files}', task.files?.join(', ') || 'Determine appropriate files')
345
+ .replace('{verification}', task.verification || 'Verify correctness')
346
+ .replace('{designContext}', `Architecture: ${plan.phases[0]?.name || 'N/A'}`)
347
+ .replace('{relevantRequirements}', task.description);
348
+ let taskSucceeded = false;
349
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
350
+ options?.onProgress?.(phase.name, task.description, iteration);
351
+ try {
352
+ const llmResult = await this.engine.runPrompt(taskPrompt, this.model);
353
+ this.totalCost += llmResult.cost;
354
+ if (llmResult.error) {
355
+ result.errors.push(`Task ${task.id} iteration ${iteration}: ${llmResult.error}`);
356
+ continue;
357
+ }
358
+ // Track files changed by the engine
359
+ if (llmResult.filesChanged > 0) {
360
+ taskSucceeded = true;
361
+ result.filesModified.push(...this.detectModifiedFiles(task.files));
362
+ break;
363
+ }
364
+ // If the engine reports no file changes but no error, consider it informational
365
+ if (llmResult.text && llmResult.text.length > 0) {
366
+ taskSucceeded = true;
367
+ break;
368
+ }
369
+ }
370
+ catch (err) {
371
+ const errorMsg = err instanceof Error ? err.message : String(err);
372
+ result.errors.push(`Task ${task.id} iteration ${iteration}: ${errorMsg}`);
373
+ }
374
+ }
375
+ if (taskSucceeded) {
376
+ task.completed = true;
377
+ result.tasksCompleted++;
378
+ }
379
+ else {
380
+ allTasksComplete = false;
381
+ result.errors.push(`Task ${task.id} failed after ${maxIterations} iterations`);
382
+ }
383
+ }
384
+ if (allTasksComplete) {
385
+ result.phasesCompleted++;
386
+ }
387
+ else {
388
+ result.success = false;
389
+ }
390
+ // Optional test run after phase
391
+ if (options?.testAfterPhase) {
392
+ const testResult = this.runProjectTests();
393
+ if (!testResult.passed) {
394
+ result.errors.push(`Tests failed after phase "${phase.name}": ${testResult.output}`);
395
+ result.success = false;
396
+ }
397
+ }
398
+ }
399
+ result.durationMs = Date.now() - startTime;
400
+ // Update totals (already set at initialization, but recalculate in case phases were filtered)
401
+ result.tasksTotal = result.tasksTotal || plan.phases.reduce((sum, p) => sum + p.tasks.length, 0);
402
+ return result;
403
+ }
404
+ // -------------------------------------------------------------------
405
+ // Pipeline Stage 5: Verify Implementation
406
+ // -------------------------------------------------------------------
407
+ async verifyImplementation(spec) {
408
+ const criteriaList = spec.requirements.flatMap(req => req.acceptanceCriteria.map((criteria, index) => ({
409
+ requirementId: req.id,
410
+ criteriaIndex: index,
411
+ criteriaText: criteria,
412
+ })));
413
+ // Gather current project state
414
+ const projectState = this.gatherProjectState(spec);
415
+ const userPrompt = [
416
+ `Verify the following implementation against its specification.`,
417
+ ``,
418
+ `== REQUIREMENTS ==`,
419
+ spec.requirements.map(r => `${r.id} (${r.priority}): ${r.title}\n ${r.description}\n Acceptance Criteria:\n${r.acceptanceCriteria.map((ac, i) => ` [${i}] ${ac}`).join('\n')}`).join('\n\n'),
420
+ ``,
421
+ `== DESIGN ==`,
422
+ `Architecture: ${spec.design.architecture}`,
423
+ `Components: ${spec.design.components.map(c => c.name).join(', ')}`,
424
+ ``,
425
+ `== CURRENT PROJECT STATE ==`,
426
+ projectState,
427
+ ``,
428
+ `For each acceptance criterion, determine if it is met by the current implementation.`,
429
+ `Provide evidence from the code for your determination.`,
430
+ ].join('\n');
431
+ const response = await this.callLLM(VERIFICATION_SYSTEM_PROMPT, userPrompt);
432
+ let parsed;
433
+ try {
434
+ const raw = this.parseJSON(response);
435
+ parsed = {
436
+ passed: raw.passed ?? false,
437
+ criteriaResults: Array.isArray(raw.criteriaResults)
438
+ ? raw.criteriaResults.map(cr => ({
439
+ requirementId: cr.requirementId || '',
440
+ criteriaIndex: cr.criteriaIndex ?? 0,
441
+ criteriaText: cr.criteriaText || '',
442
+ passed: cr.passed ?? false,
443
+ evidence: cr.evidence || '',
444
+ }))
445
+ : [],
446
+ overallScore: typeof raw.overallScore === 'number' ? raw.overallScore : 0,
447
+ issues: Array.isArray(raw.issues) ? raw.issues : [],
448
+ suggestions: Array.isArray(raw.suggestions) ? raw.suggestions : [],
449
+ };
450
+ }
451
+ catch {
452
+ // Fallback: perform a basic check based on file existence
453
+ parsed = {
454
+ passed: false,
455
+ criteriaResults: criteriaList.map(cl => ({
456
+ requirementId: cl.requirementId,
457
+ criteriaIndex: cl.criteriaIndex,
458
+ criteriaText: cl.criteriaText,
459
+ passed: false,
460
+ evidence: 'Could not verify automatically - LLM response parsing failed',
461
+ })),
462
+ overallScore: 0,
463
+ issues: ['Failed to parse verification response from LLM'],
464
+ suggestions: ['Manually verify the implementation against the spec'],
465
+ };
466
+ }
467
+ // Update requirement statuses based on verification
468
+ for (const req of spec.requirements) {
469
+ const relevantResults = parsed.criteriaResults.filter(cr => cr.requirementId === req.id);
470
+ if (relevantResults.length > 0 && relevantResults.every(cr => cr.passed)) {
471
+ req.status = 'verified';
472
+ }
473
+ else if (relevantResults.some(cr => cr.passed)) {
474
+ req.status = 'implemented';
475
+ }
476
+ else {
477
+ req.status = 'failed';
478
+ }
479
+ }
480
+ return parsed;
481
+ }
482
+ // -------------------------------------------------------------------
483
+ // Full Pipeline
484
+ // -------------------------------------------------------------------
485
+ async runFullPipeline(prompt, options) {
486
+ const startTime = Date.now();
487
+ const stages = [];
488
+ let stageStart;
489
+ let stageCost;
490
+ // Stage 1: Generate Requirements
491
+ options?.onStageChange?.('requirements', 'Generating requirements from prompt');
492
+ stageStart = Date.now();
493
+ stageCost = this.totalCost;
494
+ const requirements = await this.generateRequirements(prompt);
495
+ stages.push({
496
+ stage: 'requirements',
497
+ success: requirements.length > 0,
498
+ durationMs: Date.now() - stageStart,
499
+ cost: this.totalCost - stageCost,
500
+ details: `Generated ${requirements.length} requirements`,
501
+ });
502
+ // Stage 2: Generate Design
503
+ options?.onStageChange?.('design', 'Generating technical design from requirements');
504
+ stageStart = Date.now();
505
+ stageCost = this.totalCost;
506
+ const design = await this.generateDesign(requirements);
507
+ stages.push({
508
+ stage: 'design',
509
+ success: design.architecture.length > 0,
510
+ durationMs: Date.now() - stageStart,
511
+ cost: this.totalCost - stageCost,
512
+ details: `Generated design with ${design.components.length} components`,
513
+ });
514
+ // Stage 3: Generate Implementation Plan
515
+ options?.onStageChange?.('plan', 'Generating implementation plan from design');
516
+ stageStart = Date.now();
517
+ stageCost = this.totalCost;
518
+ const plan = await this.generatePlan(design);
519
+ const totalTasks = plan.phases.reduce((sum, p) => sum + p.tasks.length, 0);
520
+ stages.push({
521
+ stage: 'plan',
522
+ success: plan.phases.length > 0,
523
+ durationMs: Date.now() - stageStart,
524
+ cost: this.totalCost - stageCost,
525
+ details: `Generated ${plan.phases.length} phases with ${totalTasks} tasks`,
526
+ });
527
+ // Build the spec
528
+ const spec = {
529
+ id: `spec-${randomUUID().substring(0, 8)}`,
530
+ name: this.extractFeatureName(prompt),
531
+ status: 'draft',
532
+ requirements,
533
+ design,
534
+ implementationPlan: plan,
535
+ verification: {
536
+ acceptanceCriteriaMet: false,
537
+ testsPass: false,
538
+ codeReviewComplete: false,
539
+ notes: [],
540
+ },
541
+ createdAt: new Date(),
542
+ updatedAt: new Date(),
543
+ requirementsHash: this.hashContent(JSON.stringify(requirements)),
544
+ designHash: this.hashContent(JSON.stringify(design)),
545
+ originalPrompt: prompt,
546
+ currentPhaseIndex: 0,
547
+ currentTaskIndex: 0,
548
+ };
549
+ // Save the draft spec
550
+ await this.saveSpec(spec);
551
+ // Stage 4: Approval
552
+ options?.onStageChange?.('approval', 'Waiting for spec approval');
553
+ stageStart = Date.now();
554
+ stageCost = this.totalCost;
555
+ if (options?.autoApprove) {
556
+ spec.status = 'approved';
557
+ spec.updatedAt = new Date();
558
+ await this.saveSpec(spec);
559
+ }
560
+ else {
561
+ // In non-auto mode, the user should call approveSpec() manually.
562
+ // We save the draft and return with a partial result.
563
+ stages.push({
564
+ stage: 'approval',
565
+ success: false,
566
+ durationMs: Date.now() - stageStart,
567
+ cost: this.totalCost - stageCost,
568
+ details: 'Spec saved as draft - awaiting manual approval',
569
+ });
570
+ return {
571
+ spec,
572
+ executionResult: {
573
+ success: false,
574
+ phasesCompleted: 0,
575
+ tasksCompleted: 0,
576
+ tasksTotal: spec.implementationPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0),
577
+ errors: ['Spec not yet approved'],
578
+ filesModified: [],
579
+ durationMs: Date.now() - startTime,
580
+ },
581
+ verificationResult: {
582
+ passed: false,
583
+ criteriaResults: [],
584
+ overallScore: 0,
585
+ issues: ['Implementation not started - spec awaiting approval'],
586
+ suggestions: ['Call approveSpec() to proceed with implementation'],
587
+ },
588
+ totalDurationMs: Date.now() - startTime,
589
+ totalCost: this.totalCost,
590
+ stages,
591
+ };
592
+ }
593
+ stages.push({
594
+ stage: 'approval',
595
+ success: true,
596
+ durationMs: Date.now() - stageStart,
597
+ cost: this.totalCost - stageCost,
598
+ details: 'Spec auto-approved',
599
+ });
600
+ // Stage 5: Implementation
601
+ options?.onStageChange?.('implementation', 'Executing implementation plan');
602
+ stageStart = Date.now();
603
+ stageCost = this.totalCost;
604
+ spec.status = 'implementing';
605
+ spec.updatedAt = new Date();
606
+ await this.saveSpec(spec);
607
+ const executionResult = await this.executePlan(plan, {
608
+ autoApprove: true,
609
+ maxIterations: 3,
610
+ onProgress: (phase, task, iteration) => {
611
+ options?.onStageChange?.('implementation', `${phase} > ${task} (iteration ${iteration})`);
612
+ },
613
+ testAfterPhase: true,
614
+ });
615
+ stages.push({
616
+ stage: 'implementation',
617
+ success: executionResult.success,
618
+ durationMs: Date.now() - stageStart,
619
+ cost: this.totalCost - stageCost,
620
+ details: `Completed ${executionResult.tasksCompleted}/${executionResult.tasksTotal} tasks`,
621
+ });
622
+ if (executionResult.success) {
623
+ spec.status = 'complete';
624
+ }
625
+ spec.updatedAt = new Date();
626
+ await this.saveSpec(spec);
627
+ // Stage 6: Verification
628
+ options?.onStageChange?.('verification', 'Verifying implementation against spec');
629
+ stageStart = Date.now();
630
+ stageCost = this.totalCost;
631
+ const verificationResult = await this.verifyImplementation(spec);
632
+ stages.push({
633
+ stage: 'verification',
634
+ success: verificationResult.passed,
635
+ durationMs: Date.now() - stageStart,
636
+ cost: this.totalCost - stageCost,
637
+ details: `Score: ${verificationResult.overallScore}/100, ${verificationResult.issues.length} issues`,
638
+ });
639
+ // Update verification checklist
640
+ spec.verification = {
641
+ acceptanceCriteriaMet: verificationResult.passed,
642
+ testsPass: verificationResult.criteriaResults.every(cr => cr.passed),
643
+ codeReviewComplete: false,
644
+ notes: verificationResult.suggestions,
645
+ };
646
+ spec.updatedAt = new Date();
647
+ await this.saveSpec(spec);
648
+ return {
649
+ spec,
650
+ executionResult,
651
+ verificationResult,
652
+ totalDurationMs: Date.now() - startTime,
653
+ totalCost: this.totalCost,
654
+ stages,
655
+ };
656
+ }
657
+ // -------------------------------------------------------------------
658
+ // Spec Management
659
+ // -------------------------------------------------------------------
660
+ async saveSpec(spec) {
661
+ this.ensureSpecsDir();
662
+ const filePath = this.getSpecFilePath(spec.id);
663
+ const content = this.serializeSpecToMarkdown(spec);
664
+ writeFileSync(filePath, content, 'utf-8');
665
+ }
666
+ async loadSpec(id) {
667
+ const filePath = this.getSpecFilePath(id);
668
+ if (!existsSync(filePath)) {
669
+ throw new Error(`Spec not found: ${id}`);
670
+ }
671
+ const content = readFileSync(filePath, 'utf-8');
672
+ return this.deserializeSpecFromMarkdown(content);
673
+ }
674
+ async listSpecs() {
675
+ this.ensureSpecsDir();
676
+ const summaries = [];
677
+ if (!existsSync(this.specsDir))
678
+ return summaries;
679
+ const files = readdirSync(this.specsDir).filter(f => f.endsWith(SPEC_FILE_EXTENSION));
680
+ for (const file of files) {
681
+ try {
682
+ const content = readFileSync(join(this.specsDir, file), 'utf-8');
683
+ const spec = this.deserializeSpecFromMarkdown(content);
684
+ const totalTasks = spec.implementationPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0);
685
+ const completedTasks = spec.implementationPlan.phases.reduce((sum, p) => sum + p.tasks.filter(t => t.completed).length, 0);
686
+ summaries.push({
687
+ id: spec.id,
688
+ name: spec.name,
689
+ status: spec.status,
690
+ createdAt: spec.createdAt,
691
+ updatedAt: spec.updatedAt,
692
+ requirementCount: spec.requirements.length,
693
+ completedTaskCount: completedTasks,
694
+ totalTaskCount: totalTasks,
695
+ });
696
+ }
697
+ catch {
698
+ // Skip malformed spec files
699
+ }
700
+ }
701
+ return summaries.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
702
+ }
703
+ async approveSpec(id) {
704
+ const spec = await this.loadSpec(id);
705
+ if (spec.status !== 'draft' && spec.status !== 'rejected') {
706
+ throw new Error(`Cannot approve spec in "${spec.status}" status. Only draft or rejected specs can be approved.`);
707
+ }
708
+ spec.status = 'approved';
709
+ spec.updatedAt = new Date();
710
+ spec.rejectionReason = undefined;
711
+ await this.saveSpec(spec);
712
+ }
713
+ async rejectSpec(id, reason) {
714
+ const spec = await this.loadSpec(id);
715
+ if (spec.status === 'complete') {
716
+ throw new Error('Cannot reject a completed spec.');
717
+ }
718
+ spec.status = 'rejected';
719
+ spec.rejectionReason = reason;
720
+ spec.updatedAt = new Date();
721
+ await this.saveSpec(spec);
722
+ }
723
+ async deleteSpec(id) {
724
+ const filePath = this.getSpecFilePath(id);
725
+ if (existsSync(filePath)) {
726
+ unlinkSync(filePath);
727
+ }
728
+ }
729
+ // -------------------------------------------------------------------
730
+ // Acceptance Criteria Checking
731
+ // -------------------------------------------------------------------
732
+ async checkAcceptanceCriteria(spec) {
733
+ const results = [];
734
+ for (const req of spec.requirements) {
735
+ for (let i = 0; i < req.acceptanceCriteria.length; i++) {
736
+ const criteria = req.acceptanceCriteria[i];
737
+ const checkResult = await this.checkSingleCriteria(req.id, i, criteria, spec);
738
+ results.push(checkResult);
739
+ }
740
+ }
741
+ return results;
742
+ }
743
+ async checkSingleCriteria(requirementId, criteriaIndex, criteriaText, spec) {
744
+ // Gather relevant files from the spec's implementation plan
745
+ const relevantFiles = this.gatherRelevantFiles(spec);
746
+ const prompt = [
747
+ `Check if the following acceptance criterion is met:`,
748
+ ``,
749
+ `Criterion: ${criteriaText}`,
750
+ `Requirement ID: ${requirementId}`,
751
+ ``,
752
+ `Relevant project files:`,
753
+ relevantFiles,
754
+ ``,
755
+ `Respond with a JSON object:`,
756
+ `{ "passed": true/false, "evidence": "explanation of why it passed or failed" }`,
757
+ ].join('\n');
758
+ try {
759
+ const response = await this.callLLM('You are a QA verification assistant. Check if acceptance criteria are met by examining the codebase.', prompt);
760
+ const parsed = this.parseJSON(response);
761
+ return {
762
+ requirementId,
763
+ criteriaIndex,
764
+ criteriaText,
765
+ passed: parsed.passed ?? false,
766
+ evidence: parsed.evidence || 'No evidence provided',
767
+ };
768
+ }
769
+ catch {
770
+ return {
771
+ requirementId,
772
+ criteriaIndex,
773
+ criteriaText,
774
+ passed: false,
775
+ evidence: 'Failed to verify automatically',
776
+ };
777
+ }
778
+ }
779
+ // -------------------------------------------------------------------
780
+ // Spec vs Implementation Diff
781
+ // -------------------------------------------------------------------
782
+ async diffSpecVsImplementation(spec) {
783
+ // Collect all files mentioned in the spec's implementation plan
784
+ const specFiles = new Set();
785
+ for (const phase of spec.implementationPlan.phases) {
786
+ for (const task of phase.tasks) {
787
+ if (task.files) {
788
+ for (const f of task.files) {
789
+ specFiles.add(f);
790
+ }
791
+ }
792
+ }
793
+ }
794
+ // Also add files from design components
795
+ for (const component of spec.design.components) {
796
+ for (const iface of component.interfaces) {
797
+ // Extract file-like paths from interface descriptions
798
+ const fileMatch = iface.match(/[\w/.-]+\.\w+/g);
799
+ if (fileMatch) {
800
+ for (const f of fileMatch) {
801
+ specFiles.add(f);
802
+ }
803
+ }
804
+ }
805
+ }
806
+ const specFileArray = Array.from(specFiles);
807
+ // Check which files actually exist in the project
808
+ const actualFiles = [];
809
+ const contentDiffs = [];
810
+ let matchingCount = 0;
811
+ for (const specFile of specFileArray) {
812
+ const fullPath = join(this.projectRoot, specFile);
813
+ if (existsSync(fullPath)) {
814
+ actualFiles.push(specFile);
815
+ // Read actual content and compare with spec expectations
816
+ try {
817
+ const actualContent = readFileSync(fullPath, 'utf-8');
818
+ const specExpectation = this.getSpecExpectationForFile(spec, specFile);
819
+ const diff = this.computeFileDiff(specExpectation, actualContent, specFile);
820
+ contentDiffs.push(diff);
821
+ if (diff.matches)
822
+ matchingCount++;
823
+ }
824
+ catch {
825
+ contentDiffs.push({
826
+ file: specFile,
827
+ specExpectation: 'Could not read spec expectation',
828
+ actualContent: 'Could not read file',
829
+ matches: false,
830
+ differences: ['File exists but could not be read'],
831
+ });
832
+ }
833
+ }
834
+ }
835
+ // Find extra files that exist but weren't in the spec
836
+ const specFileSet = new Set(specFileArray.map(f => resolve(this.projectRoot, f)));
837
+ const extraFiles = this.findProjectSourceFiles()
838
+ .filter(f => !specFileSet.has(f));
839
+ const missingFiles = specFileArray.filter(f => !existsSync(join(this.projectRoot, f)));
840
+ const coveragePercentage = specFileArray.length > 0
841
+ ? Math.round((actualFiles.length / specFileArray.length) * 100)
842
+ : 100;
843
+ return {
844
+ specFiles: specFileArray,
845
+ actualFiles,
846
+ missingFiles,
847
+ extraFiles,
848
+ contentDiffs,
849
+ coveragePercentage,
850
+ };
851
+ }
852
+ // -------------------------------------------------------------------
853
+ // Resume implementation from where we left off
854
+ // -------------------------------------------------------------------
855
+ async resumeImplementation(id, options) {
856
+ const spec = await this.loadSpec(id);
857
+ if (spec.status !== 'approved' && spec.status !== 'implementing') {
858
+ throw new Error(`Cannot resume implementation for spec in "${spec.status}" status. Spec must be approved or already implementing.`);
859
+ }
860
+ spec.status = 'implementing';
861
+ spec.updatedAt = new Date();
862
+ await this.saveSpec(spec);
863
+ // Find where we left off
864
+ const phaseIndices = [];
865
+ let startPhaseIndex = spec.currentPhaseIndex;
866
+ for (let i = startPhaseIndex; i < spec.implementationPlan.phases.length; i++) {
867
+ phaseIndices.push(i);
868
+ }
869
+ const result = await this.executePlan(spec.implementationPlan, {
870
+ ...options,
871
+ resume: true,
872
+ phases: phaseIndices,
873
+ });
874
+ if (result.success) {
875
+ spec.status = 'complete';
876
+ }
877
+ spec.updatedAt = new Date();
878
+ await this.saveSpec(spec);
879
+ return result;
880
+ }
881
+ // -------------------------------------------------------------------
882
+ // Serialization: Spec -> Markdown with YAML frontmatter
883
+ // -------------------------------------------------------------------
884
+ serializeSpecToMarkdown(spec) {
885
+ const frontmatter = {
886
+ id: spec.id,
887
+ name: spec.name,
888
+ created: spec.createdAt.toISOString().split('T')[0],
889
+ updated: spec.updatedAt.toISOString().split('T')[0],
890
+ status: spec.status,
891
+ requirements_hash: spec.requirementsHash,
892
+ design_hash: spec.designHash,
893
+ current_phase: spec.currentPhaseIndex,
894
+ current_task: spec.currentTaskIndex,
895
+ };
896
+ if (spec.rejectionReason) {
897
+ frontmatter.rejection_reason = spec.rejectionReason;
898
+ }
899
+ if (spec.originalPrompt) {
900
+ frontmatter.original_prompt = spec.originalPrompt;
901
+ }
902
+ const yamlLines = Object.entries(frontmatter)
903
+ .map(([key, value]) => {
904
+ if (typeof value === 'string' && value.includes(':')) {
905
+ return `${key}: "${value}"`;
906
+ }
907
+ return `${key}: ${value}`;
908
+ })
909
+ .join('\n');
910
+ const sections = [
911
+ `---`,
912
+ yamlLines,
913
+ `---`,
914
+ ``,
915
+ `# Feature: ${spec.name}`,
916
+ ``,
917
+ `## Requirements`,
918
+ ``,
919
+ ];
920
+ for (const req of spec.requirements) {
921
+ sections.push(`### ${req.id}: ${req.title}`);
922
+ sections.push(`- Priority: ${req.priority}`);
923
+ sections.push(`- Status: ${req.status}`);
924
+ sections.push(`- Description: ${req.description}`);
925
+ sections.push(`- Acceptance Criteria:`);
926
+ for (const ac of req.acceptanceCriteria) {
927
+ sections.push(` - [ ] ${ac}`);
928
+ }
929
+ sections.push('');
930
+ }
931
+ sections.push('## Design');
932
+ sections.push('');
933
+ sections.push('### Architecture');
934
+ sections.push(spec.design.architecture);
935
+ sections.push('');
936
+ sections.push('### Components');
937
+ for (const comp of spec.design.components) {
938
+ sections.push(`- **${comp.name}**: ${comp.description}`);
939
+ sections.push(` - Responsibilities: ${comp.responsibilities.join(', ')}`);
940
+ sections.push(` - Interfaces: ${comp.interfaces.join(', ')}`);
941
+ sections.push(` - Dependencies: ${comp.dependencies.join(', ')}`);
942
+ }
943
+ sections.push('');
944
+ sections.push('### Data Flow');
945
+ sections.push(spec.design.dataFlow);
946
+ sections.push('');
947
+ sections.push('### API Design');
948
+ for (const api of spec.design.apiDesign) {
949
+ sections.push(`- ${api}`);
950
+ }
951
+ sections.push('');
952
+ sections.push('### Error Handling');
953
+ sections.push(spec.design.errorHandling);
954
+ sections.push('');
955
+ sections.push('## Implementation Plan');
956
+ sections.push('');
957
+ for (const phase of spec.implementationPlan.phases) {
958
+ sections.push(`### Phase ${phase.order}: ${phase.name}`);
959
+ for (const task of phase.tasks) {
960
+ const check = task.completed ? 'x' : ' ';
961
+ sections.push(`- [${check}] ${task.id}: ${task.description}`);
962
+ if (task.files && task.files.length > 0) {
963
+ sections.push(` - Files: ${task.files.join(', ')}`);
964
+ }
965
+ if (task.verification) {
966
+ sections.push(` - Verification: ${task.verification}`);
967
+ }
968
+ }
969
+ sections.push('');
970
+ }
971
+ sections.push(`**Estimated Effort**: ${spec.implementationPlan.estimatedEffort}`);
972
+ sections.push('');
973
+ if (spec.implementationPlan.dependencies.length > 0) {
974
+ sections.push('### Dependencies');
975
+ for (const dep of spec.implementationPlan.dependencies) {
976
+ sections.push(`- ${dep}`);
977
+ }
978
+ sections.push('');
979
+ }
980
+ if (spec.implementationPlan.risks.length > 0) {
981
+ sections.push('### Risks');
982
+ for (const risk of spec.implementationPlan.risks) {
983
+ sections.push(`- **${risk.id}**: ${risk.description}`);
984
+ sections.push(` - Likelihood: ${risk.likelihood}, Impact: ${risk.impact}`);
985
+ sections.push(` - Mitigation: ${risk.mitigation}`);
986
+ }
987
+ sections.push('');
988
+ }
989
+ sections.push('## Verification');
990
+ sections.push('');
991
+ sections.push(`- [${spec.verification.acceptanceCriteriaMet ? 'x' : ' '}] All acceptance criteria met`);
992
+ sections.push(`- [${spec.verification.testsPass ? 'x' : ' '}] Tests pass`);
993
+ sections.push(`- [${spec.verification.codeReviewComplete ? 'x' : ' '}] Code review complete`);
994
+ if (spec.verification.notes.length > 0) {
995
+ sections.push('');
996
+ sections.push('### Notes');
997
+ for (const note of spec.verification.notes) {
998
+ sections.push(`- ${note}`);
999
+ }
1000
+ }
1001
+ return sections.join('\n');
1002
+ }
1003
+ // -------------------------------------------------------------------
1004
+ // Deserialization: Markdown -> Spec
1005
+ // -------------------------------------------------------------------
1006
+ deserializeSpecFromMarkdown(content) {
1007
+ // Parse YAML frontmatter
1008
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
1009
+ if (!frontmatterMatch) {
1010
+ throw new Error('Invalid spec file: missing YAML frontmatter');
1011
+ }
1012
+ const yamlText = frontmatterMatch[1];
1013
+ const frontmatter = this.parseSimpleYAML(yamlText);
1014
+ // Parse requirements
1015
+ const requirements = this.parseRequirementsSection(content);
1016
+ // Parse design
1017
+ const design = this.parseDesignSection(content);
1018
+ // Parse implementation plan
1019
+ const implementationPlan = this.parseImplementationPlanSection(content);
1020
+ // Parse verification
1021
+ const verification = this.parseVerificationSection(content);
1022
+ return {
1023
+ id: frontmatter.id || `spec-${randomUUID().substring(0, 8)}`,
1024
+ name: frontmatter.name || 'Unnamed Spec',
1025
+ status: frontmatter.status || 'draft',
1026
+ requirements,
1027
+ design,
1028
+ implementationPlan,
1029
+ verification,
1030
+ createdAt: frontmatter.created
1031
+ ? new Date(frontmatter.created)
1032
+ : new Date(),
1033
+ updatedAt: frontmatter.updated
1034
+ ? new Date(frontmatter.updated)
1035
+ : new Date(),
1036
+ requirementsHash: frontmatter.requirements_hash || this.hashContent(JSON.stringify(requirements)),
1037
+ designHash: frontmatter.design_hash || this.hashContent(JSON.stringify(design)),
1038
+ rejectionReason: frontmatter.rejection_reason,
1039
+ originalPrompt: frontmatter.original_prompt,
1040
+ currentPhaseIndex: typeof frontmatter.current_phase === 'number' ? frontmatter.current_phase : 0,
1041
+ currentTaskIndex: typeof frontmatter.current_task === 'number' ? frontmatter.current_task : 0,
1042
+ };
1043
+ }
1044
+ parseSimpleYAML(yaml) {
1045
+ const result = {};
1046
+ const lines = yaml.split('\n');
1047
+ for (const line of lines) {
1048
+ const match = line.match(/^(\w+):\s*(.+)$/);
1049
+ if (match) {
1050
+ const key = match[1];
1051
+ let value = match[2].trim();
1052
+ // Remove surrounding quotes
1053
+ if (typeof value === 'string' && value.startsWith('"') && value.endsWith('"')) {
1054
+ value = value.slice(1, -1);
1055
+ }
1056
+ // Parse booleans
1057
+ if (value === 'true')
1058
+ value = true;
1059
+ else if (value === 'false')
1060
+ value = false;
1061
+ // Parse numbers
1062
+ else if (typeof value === 'string' && /^\d+$/.test(value)) {
1063
+ value = parseInt(value, 10);
1064
+ }
1065
+ result[key] = value;
1066
+ }
1067
+ }
1068
+ return result;
1069
+ }
1070
+ parseRequirementsSection(content) {
1071
+ const requirements = [];
1072
+ const reqRegex = /### (FR-\d+): (.+)(?:\n(?!###)[\s\S]*?)(?=\n### |$)/g;
1073
+ let match;
1074
+ while ((match = reqRegex.exec(content)) !== null) {
1075
+ const id = match[1];
1076
+ const title = match[2];
1077
+ const block = match[0];
1078
+ const priorityMatch = block.match(/Priority:\s*(must|should|could)/);
1079
+ const statusMatch = block.match(/Status:\s*(pending|implemented|verified|failed)/);
1080
+ const descMatch = block.match(/Description:\s*(.+)/);
1081
+ const criteriaLines = [];
1082
+ const criteriaRegex = /- \[ \] (.+)/g;
1083
+ let criteriaMatch;
1084
+ while ((criteriaMatch = criteriaRegex.exec(block)) !== null) {
1085
+ criteriaLines.push(criteriaMatch[1]);
1086
+ }
1087
+ requirements.push({
1088
+ id,
1089
+ title,
1090
+ description: descMatch ? descMatch[1] : '',
1091
+ priority: priorityMatch?.[1] || 'should',
1092
+ acceptanceCriteria: criteriaLines,
1093
+ status: statusMatch?.[1] || 'pending',
1094
+ });
1095
+ }
1096
+ return requirements;
1097
+ }
1098
+ parseDesignSection(content) {
1099
+ const designMatch = content.match(/## Design\s*\n([\s\S]*?)(?=\n## (?:Implementation|Verification))/);
1100
+ const block = designMatch ? designMatch[1] : '';
1101
+ // Architecture
1102
+ const archMatch = block.match(/### Architecture\s*\n([\s\S]*?)(?=\n###|$)/);
1103
+ const architecture = archMatch ? archMatch[1].trim() : '';
1104
+ // Components
1105
+ const components = [];
1106
+ const compRegex = /\*\*(.+?)\*\*:\s*(.+)/g;
1107
+ const compBlock = block.match(/### Components([\s\S]*?)(?=\n###|$)/);
1108
+ if (compBlock) {
1109
+ let compMatch;
1110
+ while ((compMatch = compRegex.exec(compBlock[1])) !== null) {
1111
+ const compLines = compBlock[1].substring(compMatch.index);
1112
+ const nextCompIdx = compLines.indexOf('\n- **', 1);
1113
+ const compSection = nextCompIdx > 0 ? compLines.substring(0, nextCompIdx) : compLines;
1114
+ const respMatch = compSection.match(/Responsibilities:\s*(.+)/);
1115
+ const ifaceMatch = compSection.match(/Interfaces:\s*(.+)/);
1116
+ const depMatch = compSection.match(/Dependencies:\s*(.+)/);
1117
+ components.push({
1118
+ name: compMatch[1],
1119
+ description: compMatch[2].trim(),
1120
+ responsibilities: respMatch ? respMatch[1].split(',').map(s => s.trim()) : [],
1121
+ interfaces: ifaceMatch ? ifaceMatch[1].split(',').map(s => s.trim()) : [],
1122
+ dependencies: depMatch ? depMatch[1].split(',').map(s => s.trim()) : [],
1123
+ });
1124
+ }
1125
+ }
1126
+ // Data Flow
1127
+ const dataFlowMatch = block.match(/### Data Flow\s*\n([\s\S]*?)(?=\n###|$)/);
1128
+ const dataFlow = dataFlowMatch ? dataFlowMatch[1].trim() : '';
1129
+ // API Design
1130
+ const apiMatch = block.match(/### API Design([\s\S]*?)(?=\n###|$)/);
1131
+ const apiDesign = [];
1132
+ if (apiMatch) {
1133
+ const apiLineRegex = /- (.+)/g;
1134
+ let apiLineMatch;
1135
+ while ((apiLineMatch = apiLineRegex.exec(apiMatch[1])) !== null) {
1136
+ apiDesign.push(apiLineMatch[1]);
1137
+ }
1138
+ }
1139
+ // Error Handling
1140
+ const errorMatch = block.match(/### Error Handling\s*\n([\s\S]*?)(?=\n###|$)/);
1141
+ const errorHandling = errorMatch ? errorMatch[1].trim() : '';
1142
+ return {
1143
+ architecture,
1144
+ components,
1145
+ dataFlow,
1146
+ apiDesign,
1147
+ errorHandling,
1148
+ };
1149
+ }
1150
+ parseImplementationPlanSection(content) {
1151
+ const planMatch = content.match(/## Implementation Plan\s*\n([\s\S]*?)(?=\n## Verification|$)/);
1152
+ const block = planMatch ? planMatch[1] : '';
1153
+ const phases = [];
1154
+ const phaseRegex = /### Phase (\d+): (.+)([\s\S]*?)(?=\n### Phase|$)/g;
1155
+ let phaseMatch;
1156
+ while ((phaseMatch = phaseRegex.exec(block)) !== null) {
1157
+ const order = parseInt(phaseMatch[1], 10);
1158
+ const name = phaseMatch[2].trim();
1159
+ const phaseBlock = phaseMatch[3];
1160
+ const tasks = [];
1161
+ const taskRegex = /- \[([ x])\] (T-[\d-]+): (.+)/g;
1162
+ let taskMatch;
1163
+ while ((taskMatch = taskRegex.exec(phaseBlock)) !== null) {
1164
+ const completed = taskMatch[1] === 'x';
1165
+ const taskId = taskMatch[2];
1166
+ const taskDesc = taskMatch[3];
1167
+ // Extract files and verification from subsequent lines
1168
+ const taskBlockStart = phaseBlock.indexOf(taskMatch[0]);
1169
+ const nextTaskIdx = phaseBlock.indexOf('\n- [', taskBlockStart + 1);
1170
+ const taskSection = nextTaskIdx > 0
1171
+ ? phaseBlock.substring(taskBlockStart, nextTaskIdx)
1172
+ : phaseBlock.substring(taskBlockStart);
1173
+ const filesMatch = taskSection.match(/Files:\s*(.+)/);
1174
+ const verifMatch = taskSection.match(/Verification:\s*(.+)/);
1175
+ tasks.push({
1176
+ id: taskId,
1177
+ description: taskDesc,
1178
+ completed,
1179
+ files: filesMatch ? filesMatch[1].split(',').map(s => s.trim()) : undefined,
1180
+ verification: verifMatch ? verifMatch[1].trim() : undefined,
1181
+ });
1182
+ }
1183
+ phases.push({ name, tasks, order });
1184
+ }
1185
+ // Estimated effort
1186
+ const effortMatch = block.match(/\*\*Estimated Effort\*\*:\s*(.+)/);
1187
+ const estimatedEffort = effortMatch ? effortMatch[1].trim() : 'Unknown';
1188
+ // Dependencies
1189
+ const depMatch = block.match(/### Dependencies([\s\S]*?)(?=\n###|$)/);
1190
+ const dependencies = [];
1191
+ if (depMatch) {
1192
+ const depLineRegex = /- (.+)/g;
1193
+ let depLineMatch;
1194
+ while ((depLineMatch = depLineRegex.exec(depMatch[1])) !== null) {
1195
+ dependencies.push(depLineMatch[1]);
1196
+ }
1197
+ }
1198
+ // Risks
1199
+ const risks = [];
1200
+ const riskMatch = block.match(/### Risks([\s\S]*?)(?=\n###|$)/);
1201
+ if (riskMatch) {
1202
+ const riskRegex = /\*\*(R-\d+)\*\*:\s*(.+)\n\s+Likelihood:\s*(low|medium|high),\s*Impact:\s*(low|medium|high)\n\s+Mitigation:\s*(.+)/g;
1203
+ let riskLineMatch;
1204
+ while ((riskLineMatch = riskRegex.exec(riskMatch[1])) !== null) {
1205
+ risks.push({
1206
+ id: riskLineMatch[1],
1207
+ description: riskLineMatch[2].trim(),
1208
+ likelihood: riskLineMatch[3],
1209
+ impact: riskLineMatch[4],
1210
+ mitigation: riskLineMatch[5].trim(),
1211
+ });
1212
+ }
1213
+ }
1214
+ return {
1215
+ phases,
1216
+ estimatedEffort,
1217
+ dependencies,
1218
+ risks,
1219
+ };
1220
+ }
1221
+ parseVerificationSection(content) {
1222
+ const verMatch = content.match(/## Verification\s*\n([\s\S]*?)$/);
1223
+ if (!verMatch) {
1224
+ return {
1225
+ acceptanceCriteriaMet: false,
1226
+ testsPass: false,
1227
+ codeReviewComplete: false,
1228
+ notes: [],
1229
+ };
1230
+ }
1231
+ const block = verMatch[1];
1232
+ const criteriaMetMatch = block.match(/- \[([ x])\] All acceptance criteria met/);
1233
+ const testsMatch = block.match(/- \[([ x])\] Tests pass/);
1234
+ const reviewMatch = block.match(/- \[([ x])\] Code review complete/);
1235
+ const notes = [];
1236
+ const noteRegex = /- (.+)/g;
1237
+ let noteMatch;
1238
+ while ((noteMatch = noteRegex.exec(block)) !== null) {
1239
+ // Skip the main checklist items
1240
+ if (!noteMatch[1].includes('acceptance criteria') &&
1241
+ !noteMatch[1].includes('Tests pass') &&
1242
+ !noteMatch[1].includes('Code review')) {
1243
+ notes.push(noteMatch[1]);
1244
+ }
1245
+ }
1246
+ return {
1247
+ acceptanceCriteriaMet: criteriaMetMatch?.[1] === 'x',
1248
+ testsPass: testsMatch?.[1] === 'x',
1249
+ codeReviewComplete: reviewMatch?.[1] === 'x',
1250
+ notes,
1251
+ };
1252
+ }
1253
+ // -------------------------------------------------------------------
1254
+ // Helper methods
1255
+ // -------------------------------------------------------------------
1256
+ getSpecFilePath(id) {
1257
+ // Sanitize id for use as filename
1258
+ const safeId = id.replace(/[^a-zA-Z0-9-_]/g, '_');
1259
+ return join(this.specsDir, `${safeId}${SPEC_FILE_EXTENSION}`);
1260
+ }
1261
+ extractFeatureName(prompt) {
1262
+ // Try to extract a meaningful name from the prompt
1263
+ const trimmed = prompt.trim();
1264
+ // Common patterns: "Add X", "Implement X", "Create X", "Build X"
1265
+ const actionMatch = trimmed.match(/^(?:add|implement|create|build|develop|make|write)\s+(?:a\s+|an\s+)?(.+?)(?:\s*(?:\.|that|which|for|to|with|using))$/i);
1266
+ if (actionMatch) {
1267
+ return this.toTitleCase(actionMatch[1].trim());
1268
+ }
1269
+ // Take the first sentence or first N characters
1270
+ const firstSentence = trimmed.split(/[.!?]/)[0].trim();
1271
+ if (firstSentence.length <= 60) {
1272
+ return this.toTitleCase(firstSentence);
1273
+ }
1274
+ return this.toTitleCase(trimmed.substring(0, 50).trim() + '...');
1275
+ }
1276
+ toTitleCase(text) {
1277
+ return text
1278
+ .replace(/\b\w/g, c => c.toUpperCase())
1279
+ .replace(/\s+/g, ' ')
1280
+ .trim();
1281
+ }
1282
+ detectModifiedFiles(expectedFiles) {
1283
+ // Try to detect which files were actually modified using git
1284
+ const modified = [];
1285
+ try {
1286
+ const gitOutput = execSync('git diff --name-only HEAD 2>/dev/null', {
1287
+ cwd: this.projectRoot,
1288
+ encoding: 'utf-8',
1289
+ timeout: 5000,
1290
+ }).trim();
1291
+ if (gitOutput) {
1292
+ modified.push(...gitOutput.split('\n').filter(f => f.length > 0));
1293
+ }
1294
+ // Also check untracked files
1295
+ const untrackedOutput = execSync('git ls-files --others --exclude-standard 2>/dev/null', {
1296
+ cwd: this.projectRoot,
1297
+ encoding: 'utf-8',
1298
+ timeout: 5000,
1299
+ }).trim();
1300
+ if (untrackedOutput) {
1301
+ modified.push(...untrackedOutput.split('\n').filter(f => f.length > 0));
1302
+ }
1303
+ }
1304
+ catch {
1305
+ // Git not available; fall back to expected files
1306
+ if (expectedFiles) {
1307
+ modified.push(...expectedFiles.filter(f => existsSync(join(this.projectRoot, f))));
1308
+ }
1309
+ }
1310
+ return Array.from(new Set(modified));
1311
+ }
1312
+ gatherProjectState(spec) {
1313
+ const parts = [];
1314
+ // List files mentioned in the spec that exist
1315
+ const specFiles = new Set();
1316
+ for (const phase of spec.implementationPlan.phases) {
1317
+ for (const task of phase.tasks) {
1318
+ if (task.files) {
1319
+ for (const f of task.files) {
1320
+ specFiles.add(f);
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ for (const filePath of Array.from(specFiles)) {
1326
+ const fullPath = join(this.projectRoot, filePath);
1327
+ if (existsSync(fullPath)) {
1328
+ try {
1329
+ const stat = statSync(fullPath);
1330
+ if (stat.size < 50000) {
1331
+ const content = readFileSync(fullPath, 'utf-8');
1332
+ parts.push(`--- ${filePath} ---\n${content.substring(0, 2000)}${content.length > 2000 ? '\n... (truncated)' : ''}`);
1333
+ }
1334
+ else {
1335
+ parts.push(`--- ${filePath} --- (file too large: ${stat.size} bytes)`);
1336
+ }
1337
+ }
1338
+ catch {
1339
+ parts.push(`--- ${filePath} --- (could not read)`);
1340
+ }
1341
+ }
1342
+ else {
1343
+ parts.push(`--- ${filePath} --- (does not exist)`);
1344
+ }
1345
+ }
1346
+ // Also include a file tree of the project
1347
+ try {
1348
+ const treeOutput = execSync('find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | head -100', {
1349
+ cwd: this.projectRoot,
1350
+ encoding: 'utf-8',
1351
+ timeout: 5000,
1352
+ }).trim();
1353
+ parts.push(`\n--- Project File Tree ---\n${treeOutput}`);
1354
+ }
1355
+ catch {
1356
+ // Tree not available
1357
+ }
1358
+ return parts.join('\n\n');
1359
+ }
1360
+ gatherRelevantFiles(spec) {
1361
+ const parts = [];
1362
+ for (const phase of spec.implementationPlan.phases) {
1363
+ for (const task of phase.tasks) {
1364
+ if (task.files) {
1365
+ for (const filePath of task.files) {
1366
+ const fullPath = join(this.projectRoot, filePath);
1367
+ if (existsSync(fullPath)) {
1368
+ try {
1369
+ const content = readFileSync(fullPath, 'utf-8');
1370
+ parts.push(`--- ${filePath} ---\n${content.substring(0, 1000)}`);
1371
+ }
1372
+ catch {
1373
+ parts.push(`--- ${filePath} --- (unreadable)`);
1374
+ }
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ return parts.join('\n\n') || 'No relevant files found';
1381
+ }
1382
+ getSpecExpectationForFile(spec, filePath) {
1383
+ const parts = [];
1384
+ // Find all tasks that mention this file
1385
+ for (const phase of spec.implementationPlan.phases) {
1386
+ for (const task of phase.tasks) {
1387
+ if (task.files?.includes(filePath)) {
1388
+ parts.push(`Task ${task.id}: ${task.description}`);
1389
+ if (task.verification) {
1390
+ parts.push(` Verification: ${task.verification}`);
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ // Find component descriptions that might relate to this file
1396
+ for (const component of spec.design.components) {
1397
+ const ifaceMatch = component.interfaces.some(iface => iface.includes(filePath));
1398
+ if (ifaceMatch) {
1399
+ parts.push(`Component ${component.name}: ${component.description}`);
1400
+ parts.push(` Responsibilities: ${component.responsibilities.join('; ')}`);
1401
+ }
1402
+ }
1403
+ return parts.join('\n') || `File ${filePath} was mentioned in the implementation plan`;
1404
+ }
1405
+ computeFileDiff(specExpectation, actualContent, filePath) {
1406
+ const differences = [];
1407
+ // Simple heuristic: check if key terms from the spec appear in the actual content
1408
+ const keyTerms = specExpectation
1409
+ .split(/[\s,;:]+/)
1410
+ .filter(term => term.length > 4)
1411
+ .map(term => term.toLowerCase());
1412
+ const contentLower = actualContent.toLowerCase();
1413
+ const matchedTerms = keyTerms.filter(term => contentLower.includes(term));
1414
+ const matchRatio = keyTerms.length > 0 ? matchedTerms.length / keyTerms.length : 0;
1415
+ if (matchRatio < 0.3) {
1416
+ differences.push(`Low keyword overlap between spec expectations and actual content (${Math.round(matchRatio * 100)}%)`);
1417
+ }
1418
+ // Check for common structural elements
1419
+ if (specExpectation.includes('class') && !actualContent.includes('class ')) {
1420
+ differences.push('Spec expects a class definition but none found');
1421
+ }
1422
+ if (specExpectation.includes('interface') && !actualContent.includes('interface ')) {
1423
+ differences.push('Spec expects an interface definition but none found');
1424
+ }
1425
+ if (specExpectation.includes('function') && !actualContent.includes('function ') && !actualContent.includes('=>')) {
1426
+ differences.push('Spec expects function definitions but none found');
1427
+ }
1428
+ // Check for export statements (module should export something)
1429
+ if (specExpectation.includes('export') && !actualContent.includes('export ')) {
1430
+ differences.push('Spec expects exports but none found');
1431
+ }
1432
+ const matches = differences.length === 0 && actualContent.length > 0;
1433
+ return {
1434
+ file: filePath,
1435
+ specExpectation,
1436
+ actualContent: actualContent.substring(0, 500) + (actualContent.length > 500 ? '...' : ''),
1437
+ matches,
1438
+ differences,
1439
+ };
1440
+ }
1441
+ findProjectSourceFiles() {
1442
+ const files = [];
1443
+ try {
1444
+ const output = execSync('find . -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" 2>/dev/null | head -200', {
1445
+ cwd: this.projectRoot,
1446
+ encoding: 'utf-8',
1447
+ timeout: 10000,
1448
+ }).trim();
1449
+ if (output) {
1450
+ files.push(...output.split('\n').filter(f => f.length > 0).map(f => resolve(this.projectRoot, f)));
1451
+ }
1452
+ }
1453
+ catch {
1454
+ // find not available or failed
1455
+ }
1456
+ return files;
1457
+ }
1458
+ runProjectTests() {
1459
+ try {
1460
+ const output = execSync('npm test 2>&1', {
1461
+ cwd: this.projectRoot,
1462
+ encoding: 'utf-8',
1463
+ timeout: 60000,
1464
+ });
1465
+ return { passed: true, output: output.substring(0, 500) };
1466
+ }
1467
+ catch (err) {
1468
+ const error = err;
1469
+ const output = (error.stdout || error.stderr || error.message || '').toString();
1470
+ return { passed: false, output: output.substring(0, 500) };
1471
+ }
1472
+ }
1473
+ // -------------------------------------------------------------------
1474
+ // Utility getters
1475
+ // -------------------------------------------------------------------
1476
+ /** Get the total cost accumulated by this pipeline instance */
1477
+ getTotalCost() {
1478
+ return this.totalCost;
1479
+ }
1480
+ /** Get the specs directory path */
1481
+ getSpecsDir() {
1482
+ return this.specsDir;
1483
+ }
1484
+ }
1485
+ //# sourceMappingURL=spec-driven.js.map