@sschepis/robodev 1.0.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/ai.mjs +8 -0
- package/package.json +48 -0
- package/src/cli/cli-interface.mjs +271 -0
- package/src/config.mjs +64 -0
- package/src/core/ai-assistant.mjs +540 -0
- package/src/core/ai-provider.mjs +579 -0
- package/src/core/history-manager.mjs +330 -0
- package/src/core/system-prompt.mjs +182 -0
- package/src/custom-tools/custom-tools-manager.mjs +310 -0
- package/src/execution/tool-executor.mjs +892 -0
- package/src/lib/README.md +114 -0
- package/src/lib/adapters/console-status-adapter.mjs +48 -0
- package/src/lib/adapters/network-llm-adapter.mjs +37 -0
- package/src/lib/index.mjs +101 -0
- package/src/lib/interfaces.d.ts +98 -0
- package/src/main.mjs +61 -0
- package/src/package/package-manager.mjs +223 -0
- package/src/quality/code-validator.mjs +126 -0
- package/src/quality/quality-evaluator.mjs +248 -0
- package/src/reasoning/reasoning-system.mjs +258 -0
- package/src/structured-dev/flow-manager.mjs +321 -0
- package/src/structured-dev/implementation-planner.mjs +223 -0
- package/src/structured-dev/manifest-manager.mjs +423 -0
- package/src/structured-dev/plan-executor.mjs +113 -0
- package/src/structured-dev/project-bootstrapper.mjs +523 -0
- package/src/tools/desktop-automation-tools.mjs +172 -0
- package/src/tools/file-tools.mjs +141 -0
- package/src/tools/tool-definitions.mjs +872 -0
- package/src/ui/console-styler.mjs +503 -0
- package/src/workspace/workspace-manager.mjs +215 -0
- package/themes.json +66 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Flow Manager
|
|
2
|
+
// Enforces the Discovery -> Interface -> Implementation loop
|
|
3
|
+
// Validates transitions and interacts with the ManifestManager
|
|
4
|
+
|
|
5
|
+
import { consoleStyler } from '../ui/console-styler.mjs';
|
|
6
|
+
import { ProjectBootstrapper } from './project-bootstrapper.mjs';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import util from 'util';
|
|
11
|
+
|
|
12
|
+
const execPromise = util.promisify(exec);
|
|
13
|
+
|
|
14
|
+
export class FlowManager {
|
|
15
|
+
constructor(manifestManager) {
|
|
16
|
+
this.manifestManager = manifestManager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Phases definition
|
|
20
|
+
static PHASES = {
|
|
21
|
+
DISCOVERY: 'Discovery',
|
|
22
|
+
DESIGN_REVIEW: 'Design Review',
|
|
23
|
+
INTERFACE: 'Interface',
|
|
24
|
+
IMPLEMENTATION: 'Implementation',
|
|
25
|
+
LOCKED: 'Locked'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Initialize structured development
|
|
29
|
+
// If targetDir is provided and contains a design doc, bootstrap from it.
|
|
30
|
+
async initStructuredDev(targetDir = null) {
|
|
31
|
+
const dir = targetDir || this.manifestManager.workingDir;
|
|
32
|
+
|
|
33
|
+
// Attempt bootstrap from design document
|
|
34
|
+
const bootstrapper = new ProjectBootstrapper(this.manifestManager);
|
|
35
|
+
const bootstrapResult = await bootstrapper.bootstrap(dir);
|
|
36
|
+
|
|
37
|
+
if (bootstrapResult.bootstrapped) {
|
|
38
|
+
// Design file was found and manifest was pre-populated
|
|
39
|
+
const hooksResult = await this.initHooks();
|
|
40
|
+
return `${bootstrapResult.message}\n${hooksResult}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No design file found or manifest already exists — fall back to default init
|
|
44
|
+
consoleStyler.log('system', bootstrapResult.message);
|
|
45
|
+
const manifestResult = await this.manifestManager.initManifest();
|
|
46
|
+
const hooksResult = await this.initHooks();
|
|
47
|
+
return `${manifestResult}\n${hooksResult}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize Default Hooks
|
|
51
|
+
async initHooks() {
|
|
52
|
+
const hooksDir = path.join(this.manifestManager.workingDir, '.ai-man');
|
|
53
|
+
const hooksPath = path.join(hooksDir, 'hooks.json.example');
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(hooksDir)) {
|
|
56
|
+
await fs.promises.mkdir(hooksDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(hooksPath)) {
|
|
60
|
+
const defaultHooks = {
|
|
61
|
+
"on_lock": [
|
|
62
|
+
{ "command": "echo 'Locked feature: ${featureId}'" }
|
|
63
|
+
],
|
|
64
|
+
"on_phase_change": [
|
|
65
|
+
{ "command": "echo 'Phase changed for ${featureId}'" }
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
await fs.promises.writeFile(hooksPath, JSON.stringify(defaultHooks, null, 2), 'utf8');
|
|
69
|
+
return "Created .ai-man/hooks.json.example";
|
|
70
|
+
}
|
|
71
|
+
return "Hooks example already exists.";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Submit a technical design (Phase I)
|
|
75
|
+
// Validates that the feature is in Discovery or new
|
|
76
|
+
async submitTechnicalDesign(featureId, designDoc) {
|
|
77
|
+
if (!this.manifestManager.hasManifest()) {
|
|
78
|
+
return "Error: No manifest found. Run init_structured_dev first.";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Logic to validate design doc (placeholder for now)
|
|
82
|
+
if (!designDoc || designDoc.length < 50) {
|
|
83
|
+
return "Error: Design document is too short. Please provide a comprehensive technical design.";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Update manifest: Move to Design Review phase
|
|
87
|
+
// In a real implementation, we might save the design doc to a file
|
|
88
|
+
await this.manifestManager.addFeature(featureId, "Unknown Feature", "Active", FlowManager.PHASES.DESIGN_REVIEW, "None");
|
|
89
|
+
|
|
90
|
+
// Log snapshot
|
|
91
|
+
await this.manifestManager.updateSection('4. State Snapshots', `- [${new Date().toISOString()}] Design submitted for ${featureId}. Entering Design Review.`);
|
|
92
|
+
|
|
93
|
+
return `Technical design for ${featureId} submitted. Phase moved to ${FlowManager.PHASES.DESIGN_REVIEW}. Waiting for approval.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Approve design (Phase I.5)
|
|
97
|
+
// Moves feature from Design Review to Interface
|
|
98
|
+
async approveDesign(featureId, feedback = "") {
|
|
99
|
+
if (!this.manifestManager.hasManifest()) {
|
|
100
|
+
return "Error: No manifest found.";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const manifest = await this.manifestManager.readManifest();
|
|
104
|
+
if (!manifest.includes(`| ${featureId}`)) {
|
|
105
|
+
return `Error: Feature ${featureId} not found.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Simple check: Is it in Design Review?
|
|
109
|
+
// (For robustness, we should parse the table, but regex check is okay for now)
|
|
110
|
+
// We'll trust the manifest update to handle the transition correctly if we just set the new phase.
|
|
111
|
+
|
|
112
|
+
await this.manifestManager.addFeature(featureId, "Unknown Feature", "Active", FlowManager.PHASES.INTERFACE, "None");
|
|
113
|
+
await this.manifestManager.updateSection('4. State Snapshots', `- [${new Date().toISOString()}] Design approved for ${featureId}. Feedback: ${feedback}`);
|
|
114
|
+
|
|
115
|
+
return `Design for ${featureId} APPROVED. Phase moved to ${FlowManager.PHASES.INTERFACE}.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// Lock interfaces (Phase II)
|
|
120
|
+
// Validates that the feature is in Interface phase
|
|
121
|
+
async lockInterfaces(featureId, interfaceDefinitions) {
|
|
122
|
+
if (!this.manifestManager.hasManifest()) {
|
|
123
|
+
return "Error: No manifest found.";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Verify feature state (simplified)
|
|
127
|
+
const manifest = await this.manifestManager.readManifest();
|
|
128
|
+
if (!manifest.includes(`| ${featureId}`)) {
|
|
129
|
+
// Implicitly allow if not strictly tracked yet, or fail. Let's auto-add for flexibility.
|
|
130
|
+
await this.manifestManager.addFeature(featureId, "Unknown Feature", "Active", FlowManager.PHASES.INTERFACE, "Partial");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if feature is in Design Review (Must initiate approval first)
|
|
134
|
+
// We need a more robust way to check phase than just string inclusion if we want to be strict.
|
|
135
|
+
// For now, if the manifest line contains "Design Review", we should block.
|
|
136
|
+
const featureRow = manifest.split('\n').find(line => line.includes(`| ${featureId} `));
|
|
137
|
+
if (featureRow) {
|
|
138
|
+
const cols = featureRow.split('|').map(c => c.trim());
|
|
139
|
+
// | ID | Name | Status | Phase | Lock | Priority | Dependencies |
|
|
140
|
+
// Index 4 is Phase
|
|
141
|
+
if (cols[4] === FlowManager.PHASES.DESIGN_REVIEW) {
|
|
142
|
+
return `Error: Feature ${featureId} is in Design Review. You must call 'approve_design' before locking interfaces.`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Automated Validation
|
|
147
|
+
const validationErrors = this.validateInterfaces(interfaceDefinitions);
|
|
148
|
+
if (validationErrors.length > 0) {
|
|
149
|
+
return `Error: Interface validation failed:\n- ${validationErrors.join('\n- ')}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Logic to validate interfaces (e.g., check for .d.ts content)
|
|
153
|
+
if (!interfaceDefinitions.includes('interface') && !interfaceDefinitions.includes('type')) {
|
|
154
|
+
return "Error: No interface definitions found.";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update manifest
|
|
158
|
+
await this.manifestManager.addFeature(featureId, "Unknown Feature", "Active", FlowManager.PHASES.IMPLEMENTATION, "Interface");
|
|
159
|
+
|
|
160
|
+
// Trigger hooks
|
|
161
|
+
await this.executeHooks('on_lock', { featureId });
|
|
162
|
+
|
|
163
|
+
return `Interfaces for ${featureId} LOCKED. Phase moved to ${FlowManager.PHASES.IMPLEMENTATION}.`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
validateInterfaces(definitions) {
|
|
167
|
+
const errors = [];
|
|
168
|
+
|
|
169
|
+
// 1. Syntax Check
|
|
170
|
+
// We look for 'interface X' or 'type X =' patterns.
|
|
171
|
+
const interfaceMatch = definitions.match(/interface\s+(\w+)/g);
|
|
172
|
+
const typeMatch = definitions.match(/type\s+(\w+)\s*=/g);
|
|
173
|
+
|
|
174
|
+
if (!interfaceMatch && !typeMatch) {
|
|
175
|
+
errors.push("No 'interface' or 'type' definitions found.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Strict JSDoc Check
|
|
179
|
+
// Ensure that EVERY interface or type is immediately preceded by a JSDoc comment.
|
|
180
|
+
// We'll iterate through lines to check this relationship.
|
|
181
|
+
const lines = definitions.split('\n');
|
|
182
|
+
let expectingDef = false;
|
|
183
|
+
|
|
184
|
+
// This is a heuristic parser.
|
|
185
|
+
// A perfect parser would use the TypeScript compiler API, but that's heavy.
|
|
186
|
+
// We will scan for definitions and check if the previous non-empty lines constitute a JSDoc block.
|
|
187
|
+
|
|
188
|
+
const definedTypes = [];
|
|
189
|
+
if (interfaceMatch) interfaceMatch.forEach(m => definedTypes.push(m.split(/\s+/)[1]));
|
|
190
|
+
if (typeMatch) typeMatch.forEach(m => definedTypes.push(m.split(/\s+/)[1]));
|
|
191
|
+
|
|
192
|
+
for (const typeName of definedTypes) {
|
|
193
|
+
// Find line index of definition
|
|
194
|
+
const defIndex = lines.findIndex(l => l.includes(`interface ${typeName}`) || l.includes(`type ${typeName}`));
|
|
195
|
+
if (defIndex === -1) continue;
|
|
196
|
+
|
|
197
|
+
// Look backwards for JSDoc end '*/'
|
|
198
|
+
let foundJSDoc = false;
|
|
199
|
+
for (let i = defIndex - 1; i >= 0; i--) {
|
|
200
|
+
const line = lines[i].trim();
|
|
201
|
+
if (line === '') continue; // Skip empty lines
|
|
202
|
+
if (line.endsWith('*/')) {
|
|
203
|
+
foundJSDoc = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
// If we hit code or something else before '*/', then it's undocumented
|
|
207
|
+
if (line.length > 0) break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!foundJSDoc) {
|
|
211
|
+
errors.push(`Missing JSDoc for '${typeName}'.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return errors;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Submit critique (Phase III)
|
|
219
|
+
// Mandatory step before final implementation
|
|
220
|
+
async submitCritique(featureId, critique) {
|
|
221
|
+
if (!this.manifestManager.hasManifest()) {
|
|
222
|
+
return "Error: No manifest found.";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check for 3 flaws
|
|
226
|
+
const flaws = critique.match(/\d+\./g) || [];
|
|
227
|
+
if (flaws.length < 3) {
|
|
228
|
+
return "Error: Critique must identify at least 3 potential flaws.";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Update manifest
|
|
232
|
+
await this.manifestManager.updateSection('4. State Snapshots', `- [${new Date().toISOString()}] Critique submitted for ${featureId}`);
|
|
233
|
+
|
|
234
|
+
return `Critique accepted for ${featureId}. You may proceed with final implementation.`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Read the current manifest
|
|
238
|
+
async readManifest() {
|
|
239
|
+
return await this.manifestManager.readManifest();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Visualize Architecture (Dependency Graph)
|
|
243
|
+
async visualizeArchitecture() {
|
|
244
|
+
if (!this.manifestManager.hasManifest()) {
|
|
245
|
+
return "Error: No manifest found.";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const manifest = await this.manifestManager.readManifest();
|
|
249
|
+
const graphSection = manifest.match(/## 3. Dependency Graph([\s\S]*?)(?=##|$)/);
|
|
250
|
+
|
|
251
|
+
if (!graphSection) {
|
|
252
|
+
return "graph TD;\nError[No Dependency Graph Section Found]";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse section
|
|
256
|
+
const lines = graphSection[1].trim().split('\n');
|
|
257
|
+
let mermaid = "graph TD;\n";
|
|
258
|
+
|
|
259
|
+
// Simple parser: "- A: Description" or "- A depends on B"
|
|
260
|
+
// Better: rely on Feature Registry Dependencies column
|
|
261
|
+
|
|
262
|
+
const registrySection = manifest.match(/## 2. Feature Registry([\s\S]*?)(?=## 3|$)/);
|
|
263
|
+
if (registrySection) {
|
|
264
|
+
const rows = registrySection[1].trim().split('\n').filter(l => l.trim().startsWith('|') && !l.includes('Feature ID') && !l.includes('---'));
|
|
265
|
+
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
const cols = row.split('|').map(c => c.trim());
|
|
268
|
+
// | ID | Name | Status | Phase | Lock | Priority | Dependencies |
|
|
269
|
+
// Index 1 = ID, 2 = Name, 7 = Dependencies
|
|
270
|
+
if (cols.length >= 8) {
|
|
271
|
+
const id = cols[1];
|
|
272
|
+
const name = cols[2];
|
|
273
|
+
const deps = cols[7];
|
|
274
|
+
|
|
275
|
+
mermaid += ` ${id}["${id}: ${name}"]\n`;
|
|
276
|
+
|
|
277
|
+
if (deps && deps !== '-') {
|
|
278
|
+
const depList = deps.split(',').map(d => d.trim());
|
|
279
|
+
for (const dep of depList) {
|
|
280
|
+
mermaid += ` ${dep} --> ${id}\n`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return mermaid;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Execute External Hooks
|
|
291
|
+
async executeHooks(event, context) {
|
|
292
|
+
const hooksPath = path.join(this.manifestManager.workingDir, '.ai-man', 'hooks.json');
|
|
293
|
+
if (!fs.existsSync(hooksPath)) return; // No hooks configured
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const hooksConfig = JSON.parse(await fs.promises.readFile(hooksPath, 'utf8'));
|
|
297
|
+
const commands = hooksConfig[event];
|
|
298
|
+
|
|
299
|
+
if (commands && Array.isArray(commands)) {
|
|
300
|
+
for (const cmdObj of commands) {
|
|
301
|
+
let cmd = cmdObj.command;
|
|
302
|
+
// Interpolate context
|
|
303
|
+
for (const [key, value] of Object.entries(context)) {
|
|
304
|
+
cmd = cmd.replace(new RegExp(`\\$\{${key}\\}`, 'g'), value);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
consoleStyler.log('system', `Executing hook: ${cmd}`);
|
|
308
|
+
try {
|
|
309
|
+
const { stdout, stderr } = await execPromise(cmd, { cwd: this.manifestManager.workingDir });
|
|
310
|
+
if (stdout) console.log(`Hook Output: ${stdout}`);
|
|
311
|
+
if (stderr) console.error(`Hook Error: ${stderr}`);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
consoleStyler.log('error', `Hook failed: ${e.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
consoleStyler.log('error', `Failed to execute hooks: ${error.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Implementation Planner
|
|
2
|
+
// Analyzes the System Map to generate a parallel execution plan for multi-agent implementation.
|
|
3
|
+
|
|
4
|
+
import { ManifestManager } from './manifest-manager.mjs';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
export class ImplementationPlanner {
|
|
9
|
+
constructor(manifestManager) {
|
|
10
|
+
this.manifestManager = manifestManager;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Main entry point to create a plan
|
|
14
|
+
async createExecutionPlan(outputFile = 'implementation-plan.json', numDevelopers = 3) {
|
|
15
|
+
if (!this.manifestManager.hasManifest()) {
|
|
16
|
+
return { success: false, message: "No manifest found. Run init_structured_dev first." };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const manifest = await this.manifestManager.readManifest();
|
|
20
|
+
const features = this.parseFeatureRegistry(manifest);
|
|
21
|
+
|
|
22
|
+
if (features.length === 0) {
|
|
23
|
+
return { success: false, message: "No features found in the registry." };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const stages = this.scheduleTasks(features, numDevelopers);
|
|
28
|
+
const plan = {
|
|
29
|
+
created_at: new Date().toISOString(),
|
|
30
|
+
num_developers: numDevelopers,
|
|
31
|
+
stages: stages
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const planPath = path.join(this.manifestManager.workingDir, outputFile);
|
|
35
|
+
await fs.promises.writeFile(planPath, JSON.stringify(plan, null, 2), 'utf8');
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
message: `Execution plan created with ${stages.length} stages for ${numDevelopers} developers.`,
|
|
40
|
+
plan_path: planPath,
|
|
41
|
+
plan: plan
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { success: false, message: `Failed to create plan: ${error.message}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse the Feature Registry table from markdown
|
|
49
|
+
parseFeatureRegistry(manifestContent) {
|
|
50
|
+
const lines = manifestContent.split('\n');
|
|
51
|
+
const features = [];
|
|
52
|
+
const featureMap = new Map();
|
|
53
|
+
let inRegistry = false;
|
|
54
|
+
|
|
55
|
+
// 1. Parse Registry Table
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (line.includes('## 2. Feature Registry')) {
|
|
58
|
+
inRegistry = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (inRegistry && line.startsWith('## ')) {
|
|
62
|
+
break; // End of section
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (inRegistry && line.trim().startsWith('|') && !line.includes('Feature ID') && !line.includes('---')) {
|
|
66
|
+
const cols = line.split('|').map(c => c.trim());
|
|
67
|
+
if (cols.length >= 8) {
|
|
68
|
+
const id = cols[1];
|
|
69
|
+
const name = cols[2];
|
|
70
|
+
const status = cols[3];
|
|
71
|
+
const phase = cols[4];
|
|
72
|
+
const priority = cols[6];
|
|
73
|
+
const depsRaw = cols[7];
|
|
74
|
+
|
|
75
|
+
const dependencies = (depsRaw === '-' || !depsRaw)
|
|
76
|
+
? []
|
|
77
|
+
: depsRaw.split(',').map(d => d.trim());
|
|
78
|
+
|
|
79
|
+
const feature = {
|
|
80
|
+
id,
|
|
81
|
+
name,
|
|
82
|
+
status,
|
|
83
|
+
phase,
|
|
84
|
+
priority,
|
|
85
|
+
dependencies
|
|
86
|
+
};
|
|
87
|
+
features.push(feature);
|
|
88
|
+
featureMap.set(id, feature);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Parse Dependency Graph Section
|
|
94
|
+
// Syntax: - FEAT-A -> FEAT-B, FEAT-C (Meaning A is a dependency for B and C)
|
|
95
|
+
const graphRegex = /-\s*([A-Za-z0-9-]+)\s*->\s*(.+)/;
|
|
96
|
+
let inGraph = false;
|
|
97
|
+
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (line.includes('## 3. Dependency Graph')) {
|
|
100
|
+
inGraph = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (inGraph && line.startsWith('## ')) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (inGraph) {
|
|
108
|
+
const match = line.match(graphRegex);
|
|
109
|
+
if (match) {
|
|
110
|
+
const prerequisite = match[1].trim();
|
|
111
|
+
const dependents = match[2].split(',').map(d => d.trim());
|
|
112
|
+
|
|
113
|
+
dependents.forEach(depId => {
|
|
114
|
+
const feature = featureMap.get(depId);
|
|
115
|
+
if (feature) {
|
|
116
|
+
if (!feature.dependencies.includes(prerequisite)) {
|
|
117
|
+
feature.dependencies.push(prerequisite);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return features;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Schedule tasks into parallel stages using topological sort logic with resource constraints
|
|
129
|
+
scheduleTasks(features, numDevelopers = 3) {
|
|
130
|
+
const pendingFeatures = features.filter(f => f.status !== 'Completed');
|
|
131
|
+
const completedFeatureIds = new Set(features.filter(f => f.status === 'Completed').map(f => f.id));
|
|
132
|
+
|
|
133
|
+
// Build adjacency list and in-degree map for pending tasks
|
|
134
|
+
const graph = new Map(); // id -> [dependents]
|
|
135
|
+
const inDegree = new Map(); // id -> count of pending dependencies
|
|
136
|
+
const featureMap = new Map(); // id -> feature object
|
|
137
|
+
|
|
138
|
+
// Initialize
|
|
139
|
+
pendingFeatures.forEach(f => {
|
|
140
|
+
featureMap.set(f.id, f);
|
|
141
|
+
inDegree.set(f.id, 0);
|
|
142
|
+
if (!graph.has(f.id)) graph.set(f.id, []);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Populate graph based on dependencies
|
|
146
|
+
pendingFeatures.forEach(feature => {
|
|
147
|
+
feature.dependencies.forEach(depId => {
|
|
148
|
+
// If dependency is already completed, it doesn't block
|
|
149
|
+
if (completedFeatureIds.has(depId)) return;
|
|
150
|
+
|
|
151
|
+
// If dependency is pending, add edge
|
|
152
|
+
if (featureMap.has(depId)) {
|
|
153
|
+
if (!graph.has(depId)) graph.set(depId, []);
|
|
154
|
+
graph.get(depId).push(feature.id);
|
|
155
|
+
inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1);
|
|
156
|
+
} else {
|
|
157
|
+
// Dependency exists but not in our list (external or typo)
|
|
158
|
+
// We should treat this as a potential blocker or at least warn.
|
|
159
|
+
// For robustness, we'll log it but not block, assuming manual resolution or external dependency.
|
|
160
|
+
console.warn(`[ImplementationPlanner] Warning: Feature ${feature.id} depends on unknown/missing feature ${depId}. Ignoring dependency.`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const stages = [];
|
|
166
|
+
let readyQueue = [];
|
|
167
|
+
|
|
168
|
+
// Initial set of ready tasks (in-degree 0)
|
|
169
|
+
inDegree.forEach((count, id) => {
|
|
170
|
+
if (count === 0) readyQueue.push(id);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Simple List Scheduling Algorithm
|
|
174
|
+
// While we have tasks to schedule
|
|
175
|
+
while (readyQueue.length > 0) {
|
|
176
|
+
// Take up to numDevelopers tasks for this stage
|
|
177
|
+
// We sort by number of dependents (heuristic: prioritize tasks that unlock more work)
|
|
178
|
+
readyQueue.sort((a, b) => {
|
|
179
|
+
const depsA = graph.get(a)?.length || 0;
|
|
180
|
+
const depsB = graph.get(b)?.length || 0;
|
|
181
|
+
return depsB - depsA; // Descending
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const currentStageTasks = readyQueue.splice(0, numDevelopers);
|
|
185
|
+
|
|
186
|
+
// Map IDs to full feature objects for the plan
|
|
187
|
+
const stageTasksWithDetails = currentStageTasks.map(id => featureMap.get(id));
|
|
188
|
+
|
|
189
|
+
stages.push({
|
|
190
|
+
id: stages.length + 1,
|
|
191
|
+
tasks: stageTasksWithDetails
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Simulate completion of these tasks to find new ready tasks
|
|
195
|
+
const nextReady = [];
|
|
196
|
+
|
|
197
|
+
for (const completedId of currentStageTasks) {
|
|
198
|
+
const dependents = graph.get(completedId) || [];
|
|
199
|
+
for (const dependentId of dependents) {
|
|
200
|
+
inDegree.set(dependentId, inDegree.get(dependentId) - 1);
|
|
201
|
+
if (inDegree.get(dependentId) === 0) {
|
|
202
|
+
nextReady.push(dependentId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add newly ready tasks to the queue
|
|
208
|
+
readyQueue.push(...nextReady);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for cycles (remaining in-degrees > 0)
|
|
212
|
+
const unscheduled = [];
|
|
213
|
+
inDegree.forEach((count, id) => {
|
|
214
|
+
if (count > 0) unscheduled.push(id);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (unscheduled.length > 0) {
|
|
218
|
+
throw new Error(`Cyclic dependency or unresolvable dependencies detected for features: ${unscheduled.join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return stages;
|
|
222
|
+
}
|
|
223
|
+
}
|