ultra-dex 2.2.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +84 -128
  2. package/assets/agents/00-AGENT_INDEX.md +1 -1
  3. package/assets/docs/LAUNCH-POSTS.md +1 -1
  4. package/assets/docs/QUICK-REFERENCE.md +9 -4
  5. package/assets/docs/VISION-V2.md +1 -1
  6. package/assets/hooks/pre-commit +98 -0
  7. package/assets/saas-plan/04-Imp-Template.md +1 -1
  8. package/bin/ultra-dex.js +95 -4
  9. package/lib/commands/advanced.js +471 -0
  10. package/lib/commands/agent-builder.js +226 -0
  11. package/lib/commands/agents.js +99 -42
  12. package/lib/commands/auto-implement.js +68 -0
  13. package/lib/commands/build.js +73 -187
  14. package/lib/commands/ci-monitor.js +84 -0
  15. package/lib/commands/config.js +207 -0
  16. package/lib/commands/dashboard.js +770 -0
  17. package/lib/commands/diff.js +233 -0
  18. package/lib/commands/doctor.js +397 -0
  19. package/lib/commands/export.js +408 -0
  20. package/lib/commands/fix.js +96 -0
  21. package/lib/commands/generate.js +96 -72
  22. package/lib/commands/hooks.js +251 -76
  23. package/lib/commands/init.js +53 -1
  24. package/lib/commands/memory.js +80 -0
  25. package/lib/commands/plan.js +82 -0
  26. package/lib/commands/review.js +34 -5
  27. package/lib/commands/run.js +233 -0
  28. package/lib/commands/serve.js +177 -146
  29. package/lib/commands/state.js +354 -0
  30. package/lib/commands/swarm.js +284 -0
  31. package/lib/commands/sync.js +82 -23
  32. package/lib/commands/team.js +275 -0
  33. package/lib/commands/upgrade.js +190 -0
  34. package/lib/commands/validate.js +34 -0
  35. package/lib/commands/verify.js +81 -0
  36. package/lib/commands/watch.js +79 -0
  37. package/lib/mcp/graph.js +92 -0
  38. package/lib/mcp/memory.js +95 -0
  39. package/lib/mcp/resources.js +152 -0
  40. package/lib/mcp/server.js +34 -0
  41. package/lib/mcp/tools.js +481 -0
  42. package/lib/mcp/websocket.js +117 -0
  43. package/lib/providers/index.js +49 -4
  44. package/lib/providers/ollama.js +136 -0
  45. package/lib/providers/router.js +63 -0
  46. package/lib/quality/scanner.js +128 -0
  47. package/lib/swarm/coordinator.js +97 -0
  48. package/lib/swarm/index.js +598 -0
  49. package/lib/swarm/protocol.js +677 -0
  50. package/lib/swarm/tiers.js +485 -0
  51. package/lib/templates/custom-agent.md +10 -0
  52. package/lib/utils/files.js +14 -0
  53. package/lib/utils/graph.js +108 -0
  54. package/package.json +22 -13
@@ -0,0 +1,354 @@
1
+ /**
2
+ * ultra-dex state management commands
3
+ * align, status, watch, pre-commit, state
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import fs from 'fs/promises';
9
+ import { watch as fsWatch } from 'fs';
10
+ import path from 'path';
11
+ import { validateSafePath } from '../utils/validation.js';
12
+ import { buildGraph } from '../utils/graph.js';
13
+
14
+ // State management helpers
15
+ export async function loadState() {
16
+ try {
17
+ const content = await fs.readFile(path.resolve(process.cwd(), '.ultra/state.json'), 'utf8');
18
+ return JSON.parse(content);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export async function saveState(state) {
25
+ const ultraDir = path.resolve(process.cwd(), '.ultra');
26
+ const statePath = path.resolve(ultraDir, 'state.json');
27
+ try {
28
+ await fs.mkdir(ultraDir, { recursive: true });
29
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2));
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ export async function computeState() {
37
+ // Try to load existing state first to check schema
38
+ const existing = await loadState();
39
+ if (existing && existing.project?.mode === 'GOD_MODE') {
40
+ // In God Mode, we update the timestamp
41
+ existing.updatedAt = new Date().toISOString();
42
+ return existing;
43
+ }
44
+
45
+ // Legacy computation logic
46
+ const state = {
47
+ version: '2.4.0',
48
+ updatedAt: new Date().toISOString(),
49
+ project: { name: path.basename(process.cwd()) },
50
+ files: {},
51
+ sections: { total: 34, completed: 0, list: [] },
52
+ score: 0
53
+ };
54
+
55
+ const coreFiles = ['CONTEXT.md', 'IMPLEMENTATION-PLAN.md', 'CHECKLIST.md', 'QUICK-START.md'];
56
+ for (const file of coreFiles) {
57
+ try {
58
+ const stat = await fs.stat(path.resolve(process.cwd(), file));
59
+ state.files[file] = { exists: true, size: stat.size, modified: stat.mtime.toISOString() };
60
+ } catch {
61
+ state.files[file] = { exists: false };
62
+ }
63
+ }
64
+
65
+ try {
66
+ const plan = await fs.readFile(path.resolve(process.cwd(), 'IMPLEMENTATION-PLAN.md'), 'utf8');
67
+ const sectionRegex = /^##\s+(\d+)\.\s+(.+)$/gm;
68
+ let match;
69
+ while ((match = sectionRegex.exec(plan)) !== null) {
70
+ state.sections.list.push({ number: parseInt(match[1]), title: match[2].trim() });
71
+ }
72
+ state.sections.completed = state.sections.list.length;
73
+ } catch { /* no plan */ }
74
+
75
+ const fileScore = Object.values(state.files).filter(f => f.exists).length / coreFiles.length * 40;
76
+ const sectionScore = state.sections.completed / state.sections.total * 60;
77
+ state.score = Math.round(fileScore + sectionScore);
78
+
79
+ return state;
80
+ }
81
+
82
+ export async function updateState() {
83
+ const state = await computeState();
84
+ await saveState(state);
85
+ return state;
86
+ }
87
+
88
+ export function registerAlignCommand(program) {
89
+ program
90
+ .command('align')
91
+ .description('Quick alignment score using Code Property Graph')
92
+ .option('--strict', 'Exit with error if score < 70')
93
+ .option('--json', 'Output as JSON')
94
+ .action(async (options) => {
95
+ // 1. Compute Base State
96
+ const state = await computeState();
97
+
98
+ // 2. Compute Graph Score (God Mode)
99
+ let graphScore = 0;
100
+ let graphStats = { nodes: 0, edges: 0 };
101
+
102
+ try {
103
+ const graph = await buildGraph();
104
+ graphStats = { nodes: graph.nodes.length, edges: graph.edges.length };
105
+
106
+ // Simple heuristic: A healthy project has nodes and edges
107
+ // 10+ nodes = 20 points
108
+ // 10+ edges = 20 points
109
+ const nodesPoints = Math.min(graph.nodes.length * 2, 20);
110
+ const edgesPoints = Math.min(graph.edges.length * 2, 20);
111
+ graphScore = nodesPoints + edgesPoints;
112
+ } catch (e) {
113
+ // Graph failed
114
+ }
115
+
116
+ // 3. Combine Scores
117
+ // Legacy score (files/plan) is 60% weight, Graph is 40%
118
+ const totalScore = Math.min(Math.round((state.score * 0.6) + graphScore), 100);
119
+
120
+ if (options.json) {
121
+ console.log(JSON.stringify({
122
+ score: totalScore,
123
+ legacyScore: state.score,
124
+ graphScore,
125
+ graphStats,
126
+ files: Object.values(state.files).filter(f => f.exists).length,
127
+ sections: state.sections.completed
128
+ }));
129
+ } else {
130
+ const icon = totalScore >= 80 ? 'āœ…' : totalScore >= 50 ? 'āš ļø' : 'āŒ';
131
+ console.log(`${icon} Alignment: ${totalScore}/100`);
132
+ console.log(chalk.gray(` • Plan/Docs: ${state.score}/100`));
133
+ console.log(chalk.gray(` • Code Graph: ${graphScore}/40 (Nodes: ${graphStats.nodes}, Edges: ${graphStats.edges})`));
134
+ }
135
+
136
+ if (options.strict && totalScore < 70) {
137
+ process.exit(1);
138
+ }
139
+ });
140
+ }
141
+
142
+ export function registerStatusCommand(program) {
143
+ program
144
+ .command('status')
145
+ .description('Show current project state')
146
+ .option('--refresh', 'Refresh state before showing')
147
+ .option('--json', 'Output raw JSON')
148
+ .action(async (options) => {
149
+ if (options.refresh) {
150
+ const state = await computeState();
151
+ await saveState(state);
152
+ }
153
+
154
+ let state = await loadState();
155
+ if (!state) {
156
+ console.log(chalk.yellow('\nāš ļø No .ultra/state.json found. Generating...\n'));
157
+ state = await computeState();
158
+ await saveState(state);
159
+ }
160
+
161
+ if (options.json) {
162
+ console.log(JSON.stringify(state, null, 2));
163
+ return;
164
+ }
165
+
166
+ console.log(chalk.bold('\nšŸ“Š Ultra-Dex Status\n'));
167
+ console.log(chalk.gray('─'.repeat(50)));
168
+
169
+ if (state.project?.mode === 'GOD_MODE') {
170
+ // Render God Mode Status
171
+ console.log(chalk.cyan(` MODE: ${state.project.mode}`));
172
+ console.log(chalk.gray(` Version: ${state.project.version}`));
173
+ console.log(chalk.gray('─'.repeat(50)));
174
+
175
+ console.log(chalk.bold('\nšŸš€ Phases:'));
176
+ state.phases.forEach(phase => {
177
+ const icon = phase.status === 'completed' ? 'āœ…' : phase.status === 'in_progress' ? 'šŸ”„' : 'ā³';
178
+ console.log(` ${icon} ${chalk.bold(phase.name)}`);
179
+ phase.steps.forEach(step => {
180
+ const stepIcon = step.status === 'completed' ? chalk.green('āœ“') : chalk.gray('-');
181
+ console.log(` ${stepIcon} ${step.task}`);
182
+ });
183
+ console.log('');
184
+ });
185
+
186
+ console.log(chalk.bold('šŸ¤– Agents:'));
187
+ state.agents.registry.forEach(agent => {
188
+ const active = state.agents.active.includes(agent) ? chalk.green('(Active)') : '';
189
+ console.log(` • @${agent} ${active}`);
190
+ });
191
+
192
+ } else {
193
+ // Render Legacy Status
194
+ const scoreColor = state.score >= 80 ? 'green' : state.score >= 50 ? 'yellow' : 'red';
195
+ console.log(chalk[scoreColor](` Score: ${state.score}/100`));
196
+ console.log(chalk.gray(` Updated: ${state.updatedAt}`));
197
+ console.log(chalk.gray('─'.repeat(50)));
198
+
199
+ console.log(chalk.bold('\nšŸ“ Files:'));
200
+ if (state.files) {
201
+ Object.entries(state.files).forEach(([name, info]) => {
202
+ const icon = info.exists ? chalk.green('āœ“') : chalk.red('āœ—');
203
+ const size = info.exists ? chalk.gray(` (${info.size} bytes)`) : '';
204
+ console.log(` ${icon} ${name}${size}`);
205
+ });
206
+ }
207
+
208
+ console.log(chalk.bold('\nšŸ“ Sections:'));
209
+ console.log(` ${state.sections.completed}/${state.sections.total} documented`);
210
+ if (state.sections.list.length > 0) {
211
+ const recent = state.sections.list.slice(-3);
212
+ recent.forEach(s => console.log(chalk.gray(` ${s.number}. ${s.title}`)));
213
+ if (state.sections.list.length > 3) {
214
+ console.log(chalk.gray(` ... and ${state.sections.list.length - 3} more`));
215
+ }
216
+ }
217
+ }
218
+ console.log('');
219
+ });
220
+ }
221
+
222
+ export function registerWatchCommand(program) {
223
+ // This is now handled by watch.js, but keeping here for legacy imports if any.
224
+ // In God Mode, watch.js replaces this.
225
+ // The bin/ultra-dex.js uses watch.js, so this might be dead code or overwritten.
226
+ // I will leave it as is or update it to be safe.
227
+ program
228
+ .command('watch-legacy') // Rename to avoid conflict if both registered
229
+ .action(() => console.log("Use 'ultra-dex watch' instead."));
230
+ }
231
+
232
+ export function registerPreCommitCommand(program) {
233
+ program
234
+ .command('pre-commit')
235
+ .description('Pre-commit hook - verify before commit')
236
+ .option('--install', 'Install git pre-commit hook')
237
+ .option('--scan', 'Include deep code quality scan in hook')
238
+ .option('--ai', 'Run AI-powered quality review on staged changes')
239
+ .option('-d, --dir <directory>', 'Project directory', '.')
240
+ .action(async (options) => {
241
+ const dirValidation = validateSafePath(options.dir, 'Project directory');
242
+ if (dirValidation !== true) {
243
+ console.log(chalk.red(dirValidation));
244
+ process.exit(1);
245
+ }
246
+
247
+ const rootDir = path.resolve(options.dir);
248
+
249
+ if (options.install) {
250
+ const hookPath = path.resolve(rootDir, '.git/hooks/pre-commit');
251
+ const scanCmd = options.scan ? '\nnpx ultra-dex validate --scan' : '';
252
+ const aiCmd = options.ai ? '\nnpx ultra-dex pre-commit --ai' : '';
253
+ const hookScript = `#!/bin/sh
254
+ # Ultra-Dex pre-commit hook
255
+ npx ultra-dex align --strict${scanCmd}${aiCmd}
256
+ if [ $? -ne 0 ]; then
257
+ echo "āŒ Ultra-Dex quality gate failed."
258
+ echo " Run 'ultra-dex review' or 'ultra-dex validate --scan' for details."
259
+ exit 1
260
+ fi
261
+ `;
262
+ try {
263
+ await fs.mkdir(path.dirname(hookPath), { recursive: true });
264
+ await fs.writeFile(hookPath, hookScript, { mode: 0o755 });
265
+ console.log(chalk.green('āœ… Pre-commit hook installed!'));
266
+ console.log(chalk.gray(' Commits will be blocked if alignment score < 70 or AI review fails.'));
267
+ } catch (e) {
268
+ console.log(chalk.red('āŒ Failed to install hook: ' + e.message));
269
+ }
270
+ return;
271
+ }
272
+
273
+ const state = await loadState();
274
+
275
+ // AI Quality Gate (God Mode)
276
+ if (options.ai) {
277
+ const spinner = (await import('ora')).default('šŸ¤– AI Quality Gate: Reviewing staged changes...').start();
278
+ try {
279
+ const { execSync } = await import('child_process');
280
+ // Get staged changes
281
+ const staged = execSync('git diff --cached --name-only', { encoding: 'utf8' }).split('\n').filter(Boolean);
282
+ if (staged.length === 0) {
283
+ spinner.succeed('No staged changes to review.');
284
+ return;
285
+ }
286
+
287
+ const { createProvider, getDefaultProvider } = await import('../providers/index.js');
288
+ const { runAgentLoop } = await import('./run.js');
289
+
290
+ const provider = createProvider(getDefaultProvider());
291
+ const reviewResult = await runAgentLoop('reviewer', `Review these staged files for architectural violations (e.g. missing validation, security risks):\n${staged.join('\n')}`, provider, { state });
292
+
293
+ if (reviewResult.toLowerCase().includes('reject') || reviewResult.toLowerCase().includes('blocking violation')) {
294
+ spinner.fail('AI Quality Gate: REJECTED');
295
+ console.log(chalk.red('\nviolations found:'));
296
+ console.log(reviewResult);
297
+ process.exit(1);
298
+ }
299
+ spinner.succeed('AI Quality Gate: PASSED');
300
+ } catch (e) {
301
+ spinner.warn('AI Quality Gate skipped: ' + e.message);
302
+ }
303
+ }
304
+
305
+ if (state && state.project?.mode === 'GOD_MODE') {
306
+ console.log(chalk.green(`āœ… Alignment OK: GOD MODE ACTIVE`));
307
+ return;
308
+ }
309
+
310
+ if (state && state.score < 70) {
311
+ console.log(chalk.red(`āŒ BLOCKED: Alignment score ${state.score}/100 (required: 70)`));
312
+ console.log(chalk.yellow(' Run `ultra-dex review` for detailed analysis.'));
313
+ process.exit(1);
314
+ } else {
315
+ console.log(chalk.green(`āœ… Alignment OK: ${state ? state.score : 'N/A'}/100`));
316
+ }
317
+ });
318
+ }
319
+
320
+ export function registerStateCommand(program) {
321
+ program
322
+ .command('state')
323
+ .description('Manage .ultra/state.json')
324
+ .option('--init', 'Initialize .ultra directory')
325
+ .option('--refresh', 'Refresh state from files')
326
+ .action(async (options) => {
327
+ if (options.init || options.refresh) {
328
+ const state = await computeState();
329
+ await saveState(state);
330
+ console.log(chalk.green('āœ… State updated'));
331
+ if (state.project?.mode === 'GOD_MODE') {
332
+ console.log(chalk.gray(` Mode: GOD_MODE`));
333
+ } else {
334
+ console.log(chalk.gray(` Score: ${state.score}/100`));
335
+ }
336
+ return;
337
+ }
338
+
339
+ const state = await loadState();
340
+ if (!state) {
341
+ console.log(chalk.yellow('No .ultra/state.json found. Run `ultra-dex state --init`'));
342
+ return;
343
+ }
344
+ console.log(JSON.stringify(state, null, 2));
345
+ });
346
+ }
347
+
348
+ export default {
349
+ registerAlignCommand,
350
+ registerStatusCommand,
351
+ registerWatchCommand,
352
+ registerPreCommitCommand,
353
+ registerStateCommand,
354
+ };
@@ -0,0 +1,284 @@
1
+ // cli/lib/commands/swarm.js
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getProvider } from '../providers/index.js';
5
+ import { readFile, writeFile, mkdir } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { glob } from 'glob';
9
+ import { projectGraph } from '../mcp/graph.js';
10
+ import { updateState, loadState, saveState } from './state.js';
11
+
12
+ const AGENT_PIPELINE = [
13
+ { name: 'planner', description: 'Break down task into steps', tier: '1-planning' },
14
+ { name: 'cto', description: 'Define architecture', tier: '1-planning' },
15
+ { name: 'auth', description: 'Security & authentication review', tier: '3-security' },
16
+ { name: 'database', description: 'Design schema', tier: '2-implementation' },
17
+ { name: 'backend', description: 'Implement API', tier: '2-implementation' },
18
+ { name: 'frontend', description: 'Build UI', tier: '2-implementation' },
19
+ { name: 'testing', description: 'Write tests', tier: '4-quality' },
20
+ { name: 'reviewer', description: 'Code review', tier: '4-quality' }
21
+ ];
22
+
23
+ async function runAgent(agent, task, context, previousOutput, provider) {
24
+ const agentPrompt = await loadAgentPrompt(agent.name);
25
+ const prompt = `
26
+ ${agentPrompt}
27
+
28
+ ## Context
29
+ ${context}
30
+
31
+ ## Previous Agent Output
32
+ ${previousOutput}
33
+
34
+ ## Task
35
+ ${task}
36
+
37
+ Provide your output for the next agent in the pipeline.
38
+ `;
39
+
40
+ // Standardize provider call
41
+ const response = provider.complete
42
+ ? await provider.complete(prompt)
43
+ : await provider.generate('', prompt);
44
+
45
+ return typeof response === 'string'
46
+ ? response
47
+ : (response.content || response.text || JSON.stringify(response));
48
+ }
49
+
50
+ async function ensureLogDirectory() {
51
+ const logDir = join(process.cwd(), '.ultra-dex', 'swarm-logs');
52
+ await mkdir(logDir, { recursive: true });
53
+ return logDir;
54
+ }
55
+
56
+ async function writeSwarmLog(logDir, task, results, stats) {
57
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
58
+ const logPath = join(logDir, `swarm-${timestamp}.json`);
59
+ const logData = {
60
+ task,
61
+ timestamp: new Date().toISOString(),
62
+ stats,
63
+ results
64
+ };
65
+ await writeFile(logPath, JSON.stringify(logData, null, 2));
66
+ return logPath;
67
+ }
68
+
69
+ export async function swarmCommand(task, options) {
70
+ console.log(chalk.cyan.bold('\nšŸ Ultra-Dex Swarm Mode v3.0\n'));
71
+ console.log(chalk.white(`Task: "${task}"\n`));
72
+
73
+ const startTime = Date.now();
74
+
75
+ if (options.dryRun) {
76
+ console.log(chalk.yellow('Dry run - showing pipeline:\n'));
77
+ AGENT_PIPELINE.forEach((agent, i) => {
78
+ console.log(` ${i + 1}. @${agent.name} - ${agent.description} [${agent.tier}]`);
79
+ });
80
+ if (options.parallel) {
81
+ console.log(chalk.blue('\nā„¹ļø Parallel execution enabled for 2-implementation tier'));
82
+ }
83
+ return;
84
+ }
85
+
86
+ // Load context & Graph (God Mode)
87
+ const contextPath = join(process.cwd(), 'CONTEXT.md');
88
+ const planPath = join(process.cwd(), 'IMPLEMENTATION-PLAN.md');
89
+
90
+ let context = '';
91
+ if (existsSync(contextPath)) {
92
+ context += await readFile(contextPath, 'utf-8');
93
+ }
94
+ if (existsSync(planPath)) {
95
+ context += '\n\n' + await readFile(planPath, 'utf-8');
96
+ }
97
+
98
+ // Inject Code Graph into Context
99
+ const spinnerGraph = ora('🧠 Scanning Codebase Graph...').start();
100
+ try {
101
+ const graphSummary = await projectGraph.scan();
102
+ context += `\n\n## Codebase Graph Summary\n- Total Files: ${graphSummary.nodeCount}\n- Total Dependencies: ${graphSummary.edgeCount}\n- Files Analyzed: ${graphSummary.files.join(', ')}\n`;
103
+ spinnerGraph.succeed('Codebase Graph integrated into context.');
104
+ } catch (e) {
105
+ spinnerGraph.warn('Codebase Graph scan failed, proceeding with limited context.');
106
+ }
107
+
108
+ // Get AI provider
109
+ const provider = getProvider();
110
+ if (!provider) {
111
+ console.log(chalk.red('No AI provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_AI_KEY'));
112
+ return;
113
+ }
114
+
115
+ // Ensure log directory exists
116
+ const logDir = await ensureLogDirectory();
117
+
118
+ // Update State to indicate Swarm is running
119
+ const state = await loadState() || { project: { mode: 'GOD_MODE' }, agents: { active: [] } };
120
+ state.agents = state.agents || { active: [] };
121
+ state.updatedAt = new Date().toISOString();
122
+ await saveState(state);
123
+
124
+ // Run pipeline
125
+ let previousOutput = '';
126
+ const agentResults = [];
127
+ const agentTimings = {};
128
+
129
+ // Define execution tiers (sorted by tier number)
130
+ const executionTiers = options.parallel
131
+ ? [
132
+ { name: '1-Planning', agents: AGENT_PIPELINE.filter(a => a.tier === '1-planning'), parallel: false },
133
+ { name: '2-Implementation', agents: AGENT_PIPELINE.filter(a => a.tier === '2-implementation'), parallel: true },
134
+ { name: '3-Security', agents: AGENT_PIPELINE.filter(a => a.tier === '3-security'), parallel: false },
135
+ { name: '4-Quality', agents: AGENT_PIPELINE.filter(a => a.tier === '4-quality'), parallel: false }
136
+ ]
137
+ : [{ name: 'All', agents: AGENT_PIPELINE, parallel: false }];
138
+
139
+ for (const tier of executionTiers) {
140
+ if (tier.agents.length === 0) continue;
141
+
142
+ if (options.parallel) {
143
+ console.log(chalk.blue.bold(`\nšŸ“¦ Tier: ${tier.name}`));
144
+ }
145
+
146
+ if (tier.parallel) {
147
+ // Parallel Execution for implementation tier
148
+ const tierStart = Date.now();
149
+ const promises = tier.agents.map(async (agent) => {
150
+ const agentStart = Date.now();
151
+ const spinner = ora(`Running @${agent.name}...`).start();
152
+
153
+ // Update state with active agent
154
+ const currentState = await loadState();
155
+ if (currentState) {
156
+ currentState.agents.active.push(agent.name);
157
+ await saveState(currentState);
158
+ }
159
+
160
+ try {
161
+ const result = await runAgent(agent, task, context, previousOutput, provider);
162
+ const duration = Date.now() - agentStart;
163
+ agentTimings[agent.name] = duration;
164
+ spinner.succeed(chalk.green(` @${agent.name} complete`) + chalk.gray(` (${duration}ms)`));
165
+
166
+ // Remove active agent from state
167
+ const stateDone = await loadState();
168
+ if (stateDone) {
169
+ stateDone.agents.active = stateDone.agents.active.filter(a => a !== agent.name);
170
+ await saveState(stateDone);
171
+ }
172
+
173
+ return { agent: agent.name, result, success: true };
174
+ } catch (error) {
175
+ const duration = Date.now() - agentStart;
176
+ agentTimings[agent.name] = duration;
177
+ spinner.fail(chalk.red(` @${agent.name} failed: ${error.message}`) + chalk.gray(` (${duration}ms)`));
178
+
179
+ const stateFail = await loadState();
180
+ if (stateFail) {
181
+ stateFail.agents.active = stateFail.agents.active.filter(a => a !== agent.name);
182
+ await saveState(stateFail);
183
+ }
184
+
185
+ return { agent: agent.name, error: error.message, success: false };
186
+ }
187
+ });
188
+
189
+ const results = await Promise.all(promises);
190
+ agentResults.push(...results);
191
+ previousOutput += '\n\n' + results.filter(r => r.success).map(r => r.result).join('\n\n');
192
+ console.log(chalk.gray(` Tier completed in ${Date.now() - tierStart}ms`));
193
+
194
+ } else {
195
+ // Serial Execution
196
+ for (const agent of tier.agents) {
197
+ const agentStart = Date.now();
198
+ const spinner = ora(`Running @${agent.name}...`).start();
199
+
200
+ // Update state
201
+ const currentState = await loadState();
202
+ if (currentState) {
203
+ currentState.agents.active.push(agent.name);
204
+ await saveState(currentState);
205
+ }
206
+
207
+ try {
208
+ const result = await runAgent(agent, task, context, previousOutput, provider);
209
+ const duration = Date.now() - agentStart;
210
+ agentTimings[agent.name] = duration;
211
+ previousOutput = result;
212
+ spinner.succeed(chalk.green(` @${agent.name} complete`) + chalk.gray(` (${duration}ms)`));
213
+ console.log(chalk.gray(` → ${result.slice(0, 100).replace(/\n/g, ' ')}...`));
214
+ agentResults.push({ agent: agent.name, result, success: true });
215
+
216
+ // Remove active agent
217
+ const stateDone = await loadState();
218
+ if (stateDone) {
219
+ stateDone.agents.active = stateDone.agents.active.filter(a => a !== agent.name);
220
+ await saveState(stateDone);
221
+ }
222
+
223
+ } catch (error) {
224
+ const duration = Date.now() - agentStart;
225
+ agentTimings[agent.name] = duration;
226
+ spinner.fail(chalk.red(` @${agent.name} failed: ${error.message}`) + chalk.gray(` (${duration}ms)`));
227
+ agentResults.push({ agent: agent.name, error: error.message, success: false });
228
+
229
+ const stateFail = await loadState();
230
+ if (stateFail) {
231
+ stateFail.agents.active = stateFail.agents.active.filter(a => a !== agent.name);
232
+ await saveState(stateFail);
233
+ }
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ const totalDuration = Date.now() - startTime;
241
+ const successCount = agentResults.filter(r => r.success).length;
242
+ const failCount = agentResults.filter(r => !r.success).length;
243
+
244
+ // Final state update
245
+ await updateState();
246
+
247
+ // Write log
248
+ const stats = {
249
+ totalDuration,
250
+ agentTimings,
251
+ successCount,
252
+ failCount,
253
+ parallel: options.parallel || false
254
+ };
255
+ const logPath = await writeSwarmLog(logDir, task, agentResults, stats);
256
+
257
+ // Summary
258
+ console.log(chalk.cyan.bold('\nšŸ“Š Execution Stats:'));
259
+ console.log(chalk.white(` Total time: ${totalDuration}ms`));
260
+ console.log(chalk.green(` Succeeded: ${successCount}`) + chalk.red(` Failed: ${failCount}`));
261
+ Object.entries(agentTimings).forEach(([agent, time]) => {
262
+ console.log(chalk.gray(` • @${agent}: ${time}ms`));
263
+ });
264
+ console.log(chalk.gray(`\n Log saved: ${logPath}`));
265
+
266
+ console.log(chalk.green.bold('\nāœ… Swarm complete!\n'));
267
+ }
268
+
269
+ async function loadAgentPrompt(name) {
270
+ // Use glob to find agent file recursively
271
+ const files = await glob(`agents/**/${name}.md`, { ignore: 'node_modules/**' });
272
+
273
+ if (files.length > 0) {
274
+ return await readFile(files[0], 'utf-8');
275
+ }
276
+
277
+ // Fallback to direct check
278
+ const directPath = join(process.cwd(), 'agents', `${name}.md`);
279
+ if (existsSync(directPath)) {
280
+ return await readFile(directPath, 'utf-8');
281
+ }
282
+
283
+ return `You are the @${name} agent.`;
284
+ }