@polymorphism-tech/morph-spec 4.2.0 → 4.3.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/bin/morph-spec.js +283 -8
- package/bin/validate.js +4 -4
- package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
- package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
- package/docs/next-generation/EXECUTION-FLOW.md +274 -0
- package/docs/next-generation/META-PROMPTS.md +235 -0
- package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
- package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
- package/package.json +5 -5
- package/src/commands/agents/agents-fuse.js +96 -0
- package/src/commands/agents/micro-agent.js +112 -0
- package/src/commands/agents/spawn-team.js +69 -4
- package/src/commands/agents/squad-template.js +146 -0
- package/src/commands/analytics/analytics.js +176 -0
- package/src/commands/context/context-prime.js +63 -0
- package/src/commands/context/core-four.js +54 -0
- package/src/commands/mcp/mcp.js +102 -0
- package/src/commands/project/detect-agents.js +1 -1
- package/src/commands/project/doctor.js +573 -356
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/update.js +1 -1
- package/src/commands/state/advance-phase.js +433 -416
- package/src/commands/templates/template-render.js +80 -1
- package/src/commands/threads/thread-template.js +103 -0
- package/src/commands/threads/threads.js +261 -0
- package/src/commands/trust/trust.js +205 -0
- package/src/{orchestrator.js → core/orchestrator.js} +8 -8
- package/src/core/state/state-manager.js +18 -2
- package/src/core/workflows/workflow-detector.js +100 -2
- package/src/lib/agents/micro-agent-factory.js +161 -0
- package/src/lib/analytics/analytics-engine.js +345 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +293 -258
- package/src/lib/context/context-bundler.js +240 -0
- package/src/lib/context/context-optimizer.js +212 -0
- package/src/lib/context/context-tracker.js +273 -0
- package/src/lib/context/core-four-tracker.js +201 -0
- package/src/lib/context/mcp-optimizer.js +200 -0
- package/src/lib/execution/fusion-executor.js +304 -0
- package/src/lib/execution/parallel-executor.js +270 -0
- package/src/lib/generators/context-generator.js +3 -3
- package/src/lib/generators/recap-generator.js +2 -2
- package/src/lib/hooks/hook-executor.js +169 -0
- package/src/lib/hooks/stop-hook-executor.js +286 -0
- package/src/lib/hops/hop-composer.js +221 -0
- package/src/lib/threads/thread-coordinator.js +238 -0
- package/src/lib/threads/thread-manager.js +317 -0
- package/src/lib/tracking/artifact-trail.js +202 -0
- package/src/lib/trust/trust-manager.js +269 -0
- package/src/lib/validators/design-system/design-system-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +6 -6
- package/stacks/blazor-azure/.morph/config/agents.json +72 -3
- package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
- package/CLAUDE.md +0 -993
- package/docs/llm-interaction-config.md +0 -735
- package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
- package/src/commands/utils/migrate-state.js +0 -158
- package/src/commands/utils/upgrade.js +0 -346
- package/src/lib/validators/architecture-validator.js +0 -60
- package/src/lib/validators/content-validator.js +0 -164
- package/src/lib/validators/package-validator.js +0 -61
- package/src/lib/validators/ui-contrast-validator.js +0 -44
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
- package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
- package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
- package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
- package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
- package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
- package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
- package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
- /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
- /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
- /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
- /package/docs/{v3.0 → next-generation}/README.md +0 -0
- /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusion Executor — F-Thread best-of-N aggregation
|
|
3
|
+
*
|
|
4
|
+
* Runs N parallel agents on the same prompt, collects results,
|
|
5
|
+
* then aggregates using one of three strategies:
|
|
6
|
+
* - best-of-n: score each result, pick highest (automated)
|
|
7
|
+
* - consensus: merge common elements from all results
|
|
8
|
+
* - manual-select: present all N to user for selection
|
|
9
|
+
*
|
|
10
|
+
* Returns: { winner, allResults, scores }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createThread, startThread, completeThread, failThread, THREAD_TYPES } from '../threads/thread-manager.js';
|
|
14
|
+
import { recordEvent, generateAsciiChart } from '../analytics/analytics-engine.js';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Scoring
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Score a fusion result based on completeness, compliance, and quality
|
|
22
|
+
* @param {Object} result - Agent result object
|
|
23
|
+
* @param {Object} [rubric] - Scoring rubric
|
|
24
|
+
* @returns {number} Score 0-100
|
|
25
|
+
*/
|
|
26
|
+
export function scoreResult(result, rubric = {}) {
|
|
27
|
+
let score = 0;
|
|
28
|
+
const weights = {
|
|
29
|
+
completeness: rubric.completenessWeight || 40,
|
|
30
|
+
checkpointCompliance: rubric.complianceWeight || 30,
|
|
31
|
+
codeQuality: rubric.qualityWeight || 30
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Completeness: does it have all required deliverables?
|
|
35
|
+
if (result.deliverables) {
|
|
36
|
+
const required = rubric.requiredDeliverables?.length || 1;
|
|
37
|
+
const provided = Object.keys(result.deliverables).length;
|
|
38
|
+
score += weights.completeness * Math.min(provided / required, 1.0);
|
|
39
|
+
} else if (result.content) {
|
|
40
|
+
// Estimate completeness from content length
|
|
41
|
+
const targetLength = rubric.targetLength || 1000;
|
|
42
|
+
score += weights.completeness * Math.min(result.content.length / targetLength, 1.0);
|
|
43
|
+
} else {
|
|
44
|
+
score += weights.completeness * 0.5; // Partial credit
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Checkpoint compliance: did it follow patterns?
|
|
48
|
+
if (result.checkpointsPassed !== undefined) {
|
|
49
|
+
const total = (result.checkpointsPassed || 0) + (result.checkpointsFailed || 0);
|
|
50
|
+
const passRate = total > 0 ? result.checkpointsPassed / total : 0.8; // Assume 80% if unknown
|
|
51
|
+
score += weights.checkpointCompliance * passRate;
|
|
52
|
+
} else {
|
|
53
|
+
score += weights.checkpointCompliance * 0.8; // Default: assume compliant
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Code quality: based on error count and warnings
|
|
57
|
+
const errors = result.errors || 0;
|
|
58
|
+
const warnings = result.warnings || 0;
|
|
59
|
+
const qualityScore = Math.max(0, 1.0 - (errors * 0.2) - (warnings * 0.05));
|
|
60
|
+
score += weights.codeQuality * qualityScore;
|
|
61
|
+
|
|
62
|
+
return Math.round(score);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Aggregation Strategies
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Best-of-N aggregation: score each result, return highest scorer
|
|
71
|
+
* @param {Array} results - Array of agent results
|
|
72
|
+
* @param {Object} rubric - Scoring rubric
|
|
73
|
+
* @returns {Object} { winner, scores }
|
|
74
|
+
*/
|
|
75
|
+
export function aggregateBestOfN(results, rubric = {}) {
|
|
76
|
+
const scored = results.map((result, idx) => ({
|
|
77
|
+
index: idx,
|
|
78
|
+
agentId: result.agentId || `agent-${idx}`,
|
|
79
|
+
result,
|
|
80
|
+
score: scoreResult(result, rubric)
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
scored.sort((a, b) => b.score - a.score);
|
|
84
|
+
const winner = scored[0];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
winner: winner.result,
|
|
88
|
+
winnerId: winner.agentId,
|
|
89
|
+
winnerScore: winner.score,
|
|
90
|
+
scores: scored.map(s => ({
|
|
91
|
+
agentId: s.agentId,
|
|
92
|
+
score: s.score,
|
|
93
|
+
rank: scored.indexOf(s) + 1
|
|
94
|
+
}))
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Consensus aggregation: merge common elements from all results
|
|
100
|
+
* @param {Array} results - Array of agent results
|
|
101
|
+
* @returns {Object} { merged, commonElements }
|
|
102
|
+
*/
|
|
103
|
+
export function aggregateConsensus(results) {
|
|
104
|
+
if (results.length === 0) return { merged: {}, commonElements: [] };
|
|
105
|
+
if (results.length === 1) return { merged: results[0], commonElements: [] };
|
|
106
|
+
|
|
107
|
+
// Find common keys across all results
|
|
108
|
+
const allKeys = results.map(r => Object.keys(r));
|
|
109
|
+
const commonKeys = allKeys[0].filter(key =>
|
|
110
|
+
allKeys.every(keys => keys.includes(key))
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const merged = {};
|
|
114
|
+
const commonElements = [];
|
|
115
|
+
|
|
116
|
+
for (const key of commonKeys) {
|
|
117
|
+
const values = results.map(r => r[key]);
|
|
118
|
+
|
|
119
|
+
// For strings: use longest (most complete) version
|
|
120
|
+
if (values.every(v => typeof v === 'string')) {
|
|
121
|
+
merged[key] = values.reduce((longest, v) =>
|
|
122
|
+
v.length > longest.length ? v : longest, ''
|
|
123
|
+
);
|
|
124
|
+
commonElements.push(key);
|
|
125
|
+
}
|
|
126
|
+
// For arrays: union of all arrays
|
|
127
|
+
else if (values.every(v => Array.isArray(v))) {
|
|
128
|
+
merged[key] = [...new Set(values.flat())];
|
|
129
|
+
commonElements.push(key);
|
|
130
|
+
}
|
|
131
|
+
// For objects: merge recursively (simple one level)
|
|
132
|
+
else if (values.every(v => v && typeof v === 'object')) {
|
|
133
|
+
merged[key] = Object.assign({}, ...values);
|
|
134
|
+
commonElements.push(key);
|
|
135
|
+
}
|
|
136
|
+
// For primitives: use majority value
|
|
137
|
+
else {
|
|
138
|
+
const valueCounts = values.reduce((acc, v) => {
|
|
139
|
+
const key2 = String(v);
|
|
140
|
+
acc[key2] = (acc[key2] || 0) + 1;
|
|
141
|
+
return acc;
|
|
142
|
+
}, {});
|
|
143
|
+
const [majority] = Object.entries(valueCounts).sort(([, a], [, b]) => b - a);
|
|
144
|
+
merged[key] = majority ? JSON.parse(majority[0]) : values[0];
|
|
145
|
+
commonElements.push(key);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { merged, commonElements };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Main Fusion Runner
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run fusion execution: spawn N agents in parallel, collect and aggregate results
|
|
158
|
+
* @param {Object} opts
|
|
159
|
+
* @param {string} opts.feature - Feature name
|
|
160
|
+
* @param {string} opts.prompt - Prompt/mission for all agents
|
|
161
|
+
* @param {number} [opts.count=3] - Number of agents to run
|
|
162
|
+
* @param {string} [opts.strategy='best-of-n'] - Aggregation strategy
|
|
163
|
+
* @param {string[]} [opts.agents] - Agent IDs to use (will use thread types if not specified)
|
|
164
|
+
* @param {Object} [opts.rubric] - Scoring rubric for best-of-n
|
|
165
|
+
* @returns {Promise<Object>} { winner, allResults, scores, strategy, threads }
|
|
166
|
+
*/
|
|
167
|
+
export async function runFusion(prompt, {
|
|
168
|
+
feature,
|
|
169
|
+
count = 3,
|
|
170
|
+
strategy = 'best-of-n',
|
|
171
|
+
agents = null,
|
|
172
|
+
rubric = {}
|
|
173
|
+
} = {}) {
|
|
174
|
+
const { randomUUID } = await import('crypto');
|
|
175
|
+
const sessionId = randomUUID();
|
|
176
|
+
const agentList = agents || Array.from({ length: count }, (_, i) => `fusion-agent-${i + 1}`);
|
|
177
|
+
const threads = [];
|
|
178
|
+
const results = [];
|
|
179
|
+
|
|
180
|
+
recordEvent({
|
|
181
|
+
type: 'fusion_started',
|
|
182
|
+
feature,
|
|
183
|
+
data: { count, strategy, agents: agentList }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Create and start all fusion threads simultaneously
|
|
187
|
+
for (let i = 0; i < agentList.length; i++) {
|
|
188
|
+
const thread = createThread({
|
|
189
|
+
feature,
|
|
190
|
+
type: THREAD_TYPES.FUSION,
|
|
191
|
+
agent: agentList[i],
|
|
192
|
+
mission: prompt,
|
|
193
|
+
meta: { fusionIndex: i, strategy }
|
|
194
|
+
});
|
|
195
|
+
startThread(thread.id);
|
|
196
|
+
threads.push(thread);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(` [fusion] ${threads.length} agents spawned. Collecting results...`);
|
|
200
|
+
|
|
201
|
+
// In a real system, results would come from actual sub-agent execution.
|
|
202
|
+
// Here we return thread configs for the calling code to use with Task tool.
|
|
203
|
+
// The aggregation is designed to be called after results are collected.
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: sessionId,
|
|
207
|
+
threads,
|
|
208
|
+
strategy,
|
|
209
|
+
feature,
|
|
210
|
+
prompt,
|
|
211
|
+
agentConfigs: agentList.map((agentId, i) => ({
|
|
212
|
+
agentId,
|
|
213
|
+
threadId: threads[i].id,
|
|
214
|
+
mission: prompt
|
|
215
|
+
})),
|
|
216
|
+
aggregate: (collectedResults) => aggregateResults(collectedResults, strategy, rubric)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Aggregate collected results from N agents
|
|
222
|
+
* @param {Array} results - Array of result objects from agents
|
|
223
|
+
* @param {string} strategy - Aggregation strategy
|
|
224
|
+
* @param {Object} [rubric] - Scoring rubric for best-of-n
|
|
225
|
+
* @returns {Object} Aggregated result
|
|
226
|
+
*/
|
|
227
|
+
export function aggregateResults(results, strategy = 'best-of-n', rubric = {}) {
|
|
228
|
+
if (!results || results.length === 0) {
|
|
229
|
+
throw new Error('No results to aggregate');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let aggregated;
|
|
233
|
+
|
|
234
|
+
switch (strategy) {
|
|
235
|
+
case 'best-of-n':
|
|
236
|
+
aggregated = aggregateBestOfN(results, rubric);
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'consensus':
|
|
240
|
+
const { merged, commonElements } = aggregateConsensus(results);
|
|
241
|
+
aggregated = {
|
|
242
|
+
winner: merged,
|
|
243
|
+
winnerId: 'consensus',
|
|
244
|
+
scores: results.map((_, i) => ({
|
|
245
|
+
agentId: `agent-${i}`,
|
|
246
|
+
score: 0,
|
|
247
|
+
rank: i + 1
|
|
248
|
+
})),
|
|
249
|
+
commonElements
|
|
250
|
+
};
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'manual-select':
|
|
254
|
+
aggregated = {
|
|
255
|
+
winner: null,
|
|
256
|
+
winnerId: null,
|
|
257
|
+
allResults: results,
|
|
258
|
+
requiresUserSelection: true,
|
|
259
|
+
scores: results.map((r, i) => ({
|
|
260
|
+
agentId: r.agentId || `agent-${i}`,
|
|
261
|
+
score: scoreResult(r, rubric),
|
|
262
|
+
rank: i + 1
|
|
263
|
+
}))
|
|
264
|
+
};
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
default:
|
|
268
|
+
throw new Error(`Unknown aggregation strategy: ${strategy}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
...aggregated,
|
|
273
|
+
strategy,
|
|
274
|
+
totalAgents: results.length,
|
|
275
|
+
allResults: results
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Display fusion results summary
|
|
281
|
+
* @param {Object} fusionResult - Result from aggregateResults
|
|
282
|
+
*/
|
|
283
|
+
export function displayFusionSummary(fusionResult) {
|
|
284
|
+
const { strategy, totalAgents, scores, winner, requiresUserSelection } = fusionResult;
|
|
285
|
+
|
|
286
|
+
console.log(`\n Fusion Results — ${strategy} (${totalAgents} agents)\n`);
|
|
287
|
+
console.log(' ' + '─'.repeat(50));
|
|
288
|
+
|
|
289
|
+
if (scores) {
|
|
290
|
+
const scoreData = {};
|
|
291
|
+
scores.forEach(s => { scoreData[s.agentId] = s.score; });
|
|
292
|
+
console.log(generateAsciiChart(scoreData, { title: 'Agent Scores' }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (requiresUserSelection) {
|
|
296
|
+
console.log('\n Manual selection required — review results above');
|
|
297
|
+
} else if (winner) {
|
|
298
|
+
console.log(`\n Winner: ${fusionResult.winnerId || 'consensus'}`);
|
|
299
|
+
if (fusionResult.winnerScore !== undefined) {
|
|
300
|
+
console.log(` Score: ${fusionResult.winnerScore}/100`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Executor — P-Thread concurrent agent spawning
|
|
3
|
+
*
|
|
4
|
+
* Manages concurrent thread execution for parallel feature development.
|
|
5
|
+
* Configurable max-concurrent (1-5, default 3).
|
|
6
|
+
*
|
|
7
|
+
* Thread types supported:
|
|
8
|
+
* P-Thread — Parallel execution (multiple agents simultaneously)
|
|
9
|
+
* Managed via thread-manager.js + analytics-engine.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createThread, startThread, completeThread, failThread, listThreads, THREAD_TYPES, THREAD_STATUS } from '../threads/thread-manager.js';
|
|
13
|
+
import { recordEvent } from '../analytics/analytics-engine.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MAX_CONCURRENT = 3;
|
|
16
|
+
const MAX_CONCURRENT_LIMIT = 5;
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Parallel Execution
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Spawn N threads in parallel for the same feature
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {string} opts.feature - Feature name
|
|
26
|
+
* @param {Array} opts.threadConfigs - Array of { agent, mission, meta } configs
|
|
27
|
+
* @param {number} [opts.maxConcurrent=3] - Maximum concurrent threads
|
|
28
|
+
* @returns {Promise<Array>} Array of created thread objects
|
|
29
|
+
*/
|
|
30
|
+
export async function spawnParallel({ feature, threadConfigs, maxConcurrent = DEFAULT_MAX_CONCURRENT }) {
|
|
31
|
+
const limit = Math.min(maxConcurrent, MAX_CONCURRENT_LIMIT);
|
|
32
|
+
|
|
33
|
+
if (!threadConfigs || threadConfigs.length === 0) {
|
|
34
|
+
throw new Error('threadConfigs is required and must be non-empty');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const threads = [];
|
|
38
|
+
const batches = chunkArray(threadConfigs, limit);
|
|
39
|
+
|
|
40
|
+
recordEvent({
|
|
41
|
+
type: 'parallel_spawn_started',
|
|
42
|
+
feature,
|
|
43
|
+
data: {
|
|
44
|
+
totalThreads: threadConfigs.length,
|
|
45
|
+
maxConcurrent: limit,
|
|
46
|
+
batches: batches.length
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const batch of batches) {
|
|
51
|
+
const batchThreads = batch.map(config =>
|
|
52
|
+
createThread({
|
|
53
|
+
feature,
|
|
54
|
+
type: THREAD_TYPES.PARALLEL,
|
|
55
|
+
agent: config.agent,
|
|
56
|
+
mission: config.mission,
|
|
57
|
+
meta: { ...config.meta, maxConcurrent: limit }
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
threads.push(...batchThreads);
|
|
62
|
+
|
|
63
|
+
// Mark all batch threads as running simultaneously
|
|
64
|
+
for (const t of batchThreads) {
|
|
65
|
+
startThread(t.id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
recordEvent({
|
|
69
|
+
type: 'parallel_batch_started',
|
|
70
|
+
feature,
|
|
71
|
+
data: {
|
|
72
|
+
batchSize: batch.length,
|
|
73
|
+
threadIds: batchThreads.map(t => t.id),
|
|
74
|
+
concurrency: batch.length
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return threads;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Coordinate parallel thread lifecycle — wait for all to complete
|
|
84
|
+
* @param {string} feature - Feature name
|
|
85
|
+
* @param {string[]} threadIds - Thread IDs to track
|
|
86
|
+
* @param {Object} [opts]
|
|
87
|
+
* @param {number} [opts.pollIntervalMs=5000] - Poll interval
|
|
88
|
+
* @param {number} [opts.timeoutMs=3600000] - Max wait (1 hour default)
|
|
89
|
+
* @returns {Promise<Object>} { completed, failed, duration }
|
|
90
|
+
*/
|
|
91
|
+
export async function coordinateParallel(feature, threadIds, {
|
|
92
|
+
pollIntervalMs = 5000,
|
|
93
|
+
timeoutMs = 3600000
|
|
94
|
+
} = {}) {
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
const completed = [];
|
|
97
|
+
const failed = [];
|
|
98
|
+
const remaining = new Set(threadIds);
|
|
99
|
+
|
|
100
|
+
while (remaining.size > 0 && Date.now() - start < timeoutMs) {
|
|
101
|
+
for (const id of [...remaining]) {
|
|
102
|
+
const threads = listThreads({ feature });
|
|
103
|
+
const thread = threads.find(t => t.id === id);
|
|
104
|
+
|
|
105
|
+
if (!thread) {
|
|
106
|
+
remaining.delete(id);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (thread.status === THREAD_STATUS.COMPLETED) {
|
|
111
|
+
completed.push(id);
|
|
112
|
+
remaining.delete(id);
|
|
113
|
+
} else if (thread.status === THREAD_STATUS.FAILED || thread.status === THREAD_STATUS.KILLED) {
|
|
114
|
+
failed.push(id);
|
|
115
|
+
remaining.delete(id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (remaining.size > 0) {
|
|
120
|
+
await sleep(pollIntervalMs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const duration = Math.round((Date.now() - start) / 1000);
|
|
125
|
+
|
|
126
|
+
if (remaining.size > 0) {
|
|
127
|
+
recordEvent({
|
|
128
|
+
type: 'parallel_timeout',
|
|
129
|
+
feature,
|
|
130
|
+
data: { timedOut: [...remaining], duration }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
recordEvent({
|
|
135
|
+
type: 'parallel_coordination_complete',
|
|
136
|
+
feature,
|
|
137
|
+
data: { completed: completed.length, failed: failed.length, duration }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return { completed, failed, timedOut: [...remaining], duration };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wait until all specified threads complete
|
|
145
|
+
* @param {string} feature - Feature name
|
|
146
|
+
* @param {string[]} threadIds - Thread IDs to wait for
|
|
147
|
+
* @param {number} [timeoutMs=3600000] - Max wait time
|
|
148
|
+
* @returns {Promise<Object>} Coordination result
|
|
149
|
+
*/
|
|
150
|
+
export async function waitAll(feature, threadIds, timeoutMs = 3600000) {
|
|
151
|
+
return coordinateParallel(feature, threadIds, { timeoutMs });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Wait until any thread completes, then return it
|
|
156
|
+
* @param {string} feature - Feature name
|
|
157
|
+
* @param {string[]} threadIds - Thread IDs to watch
|
|
158
|
+
* @param {number} [timeoutMs=3600000] - Max wait time
|
|
159
|
+
* @returns {Promise<Object|null>} First completed/failed thread or null on timeout
|
|
160
|
+
*/
|
|
161
|
+
export async function waitAny(feature, threadIds, timeoutMs = 3600000) {
|
|
162
|
+
const start = Date.now();
|
|
163
|
+
const idSet = new Set(threadIds);
|
|
164
|
+
|
|
165
|
+
while (Date.now() - start < timeoutMs) {
|
|
166
|
+
const threads = listThreads({ feature });
|
|
167
|
+
for (const thread of threads) {
|
|
168
|
+
if (idSet.has(thread.id)) {
|
|
169
|
+
if (thread.status === THREAD_STATUS.COMPLETED || thread.status === THREAD_STATUS.FAILED) {
|
|
170
|
+
return thread;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await sleep(3000);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Efficiency Metrics
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Plan parallel execution batches without spawning threads
|
|
186
|
+
* @param {Object} opts
|
|
187
|
+
* @param {Array} opts.threadConfigs - Array of { agent, mission } configs
|
|
188
|
+
* @param {number} [opts.maxConcurrent=3] - Max concurrent threads
|
|
189
|
+
* @returns {Object} { batches, totalThreads, maxConcurrent, batchCount }
|
|
190
|
+
*/
|
|
191
|
+
export function planParallelBatches({ threadConfigs, maxConcurrent = DEFAULT_MAX_CONCURRENT }) {
|
|
192
|
+
const limit = Math.min(maxConcurrent, MAX_CONCURRENT_LIMIT);
|
|
193
|
+
const batches = chunkArray(threadConfigs, limit);
|
|
194
|
+
return {
|
|
195
|
+
batches,
|
|
196
|
+
totalThreads: threadConfigs.length,
|
|
197
|
+
maxConcurrent: limit,
|
|
198
|
+
batchCount: batches.length
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Calculate parallel execution efficiency
|
|
204
|
+
* @param {string|Object} featureOrOpts - Feature name, or { totalTasks, waves, maxConcurrent }
|
|
205
|
+
* @returns {Object} Efficiency metrics (efficiency is 0-1 when given opts, 0-100 for real threads)
|
|
206
|
+
*/
|
|
207
|
+
export function getParallelEfficiency(featureOrOpts) {
|
|
208
|
+
// Accept stats object for testing/preview
|
|
209
|
+
if (featureOrOpts && typeof featureOrOpts === 'object' && 'totalTasks' in featureOrOpts) {
|
|
210
|
+
const { totalTasks, waves, maxConcurrent = DEFAULT_MAX_CONCURRENT } = featureOrOpts;
|
|
211
|
+
const serialSteps = Math.ceil(totalTasks / Math.max(maxConcurrent, 1));
|
|
212
|
+
const efficiency = waves > 0 ? Math.min(1, serialSteps / waves) : 0;
|
|
213
|
+
return { totalTasks, waves, maxConcurrent, efficiency: Math.round(efficiency * 100) / 100 };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const feature = featureOrOpts;
|
|
217
|
+
const threads = listThreads({ feature, type: THREAD_TYPES.PARALLEL });
|
|
218
|
+
|
|
219
|
+
if (threads.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
feature,
|
|
222
|
+
parallelThreads: 0,
|
|
223
|
+
avgParallel: 0,
|
|
224
|
+
maxParallel: 0,
|
|
225
|
+
efficiency: 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Group by time windows to estimate concurrent execution
|
|
230
|
+
const completed = threads.filter(t => t.completedAt && t.startedAt);
|
|
231
|
+
let maxParallel = 0;
|
|
232
|
+
|
|
233
|
+
if (completed.length > 1) {
|
|
234
|
+
// Find the time when most threads were running simultaneously
|
|
235
|
+
for (const t1 of completed) {
|
|
236
|
+
const concurrent = completed.filter(t2 =>
|
|
237
|
+
t2.startedAt <= t1.completedAt && t2.completedAt >= t1.startedAt
|
|
238
|
+
).length;
|
|
239
|
+
if (concurrent > maxParallel) maxParallel = concurrent;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
maxParallel = threads.length;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const avgParallel = threads.length / Math.max(1, Math.ceil(threads.length / maxParallel));
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
feature,
|
|
249
|
+
parallelThreads: threads.length,
|
|
250
|
+
avgParallel: Math.round(avgParallel * 10) / 10,
|
|
251
|
+
maxParallel,
|
|
252
|
+
efficiency: Math.round(avgParallel / Math.max(maxParallel, 1) * 100)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// Helpers
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
260
|
+
function chunkArray(arr, size) {
|
|
261
|
+
const chunks = [];
|
|
262
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
263
|
+
chunks.push(arr.slice(i, i + size));
|
|
264
|
+
}
|
|
265
|
+
return chunks;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sleep(ms) {
|
|
269
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
270
|
+
}
|
|
@@ -50,7 +50,7 @@ async function loadConfig(projectPath) {
|
|
|
50
50
|
* @returns {Promise<Object>} Parsed agents config
|
|
51
51
|
*/
|
|
52
52
|
async function loadAgents(projectPath) {
|
|
53
|
-
const { resolveAgentsConfigPath } = await import('
|
|
53
|
+
const { resolveAgentsConfigPath } = await import('../stacks/stack-resolver.js');
|
|
54
54
|
const agentsPath = resolveAgentsConfigPath(projectPath);
|
|
55
55
|
try {
|
|
56
56
|
const content = await fs.readFile(agentsPath, 'utf8');
|
|
@@ -180,7 +180,7 @@ export async function generateProjectContext(projectPath) {
|
|
|
180
180
|
]);
|
|
181
181
|
|
|
182
182
|
// Load template with fallback (stack-specific → framework universal)
|
|
183
|
-
const { resolveTemplatePath } = await import('
|
|
183
|
+
const { resolveTemplatePath } = await import('../stacks/stack-resolver.js');
|
|
184
184
|
const templatePath = resolveTemplatePath(projectPath, 'context/CONTEXT.md');
|
|
185
185
|
|
|
186
186
|
if (!templatePath) {
|
|
@@ -352,7 +352,7 @@ export async function generateFeatureContext(projectPath, featureName) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
// Load template with fallback (stack-specific → framework universal)
|
|
355
|
-
const { resolveTemplatePath } = await import('
|
|
355
|
+
const { resolveTemplatePath } = await import('../stacks/stack-resolver.js');
|
|
356
356
|
const featureTemplatePath = resolveTemplatePath(projectPath, 'context/CONTEXT-FEATURE.md');
|
|
357
357
|
|
|
358
358
|
if (!featureTemplatePath) {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
11
11
|
import { join, dirname } from 'path';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { loadState } from '
|
|
14
|
-
import { runValidation } from '
|
|
13
|
+
import { loadState } from '../../core/state/state-manager.js';
|
|
14
|
+
import { runValidation } from '../validators/validation-runner.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Generate recap.md for a feature
|