@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.
@@ -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
+ }