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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|