@polymorphism-tech/morph-spec 4.2.0 → 4.3.1
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/CLAUDE.md +108 -946
- package/bin/morph-spec.js +284 -9
- package/bin/task-manager.cjs +102 -14
- 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 +97 -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 +32 -2
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +573 -356
- package/src/commands/project/init.js +9 -2
- package/src/commands/project/update.js +13 -3
- package/src/commands/state/advance-phase.js +448 -416
- package/src/commands/state/state.js +14 -12
- package/src/commands/tasks/task.js +1 -1
- 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 +37 -17
- package/src/core/workflows/workflow-detector.js +114 -3
- 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 +298 -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/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- 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 +32 -12
- 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 +14 -30
- package/src/utils/hooks-installer.js +69 -0
- package/stacks/blazor-azure/.morph/config/agents.json +72 -3
- package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
- 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,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
|
|
@@ -75,18 +75,38 @@ export async function generateRecap(projectPath, featureName, options = {}) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function getTasksSummary(feature) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// v2: feature.tasks is an array of task objects
|
|
79
|
+
if (Array.isArray(feature.tasks)) {
|
|
80
|
+
const tasks = feature.tasks;
|
|
81
|
+
const completed = tasks.filter(t => t.status === 'completed');
|
|
82
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
83
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
84
|
+
return {
|
|
85
|
+
total: tasks.length,
|
|
86
|
+
completed: completed.length,
|
|
87
|
+
pending: pending.length,
|
|
88
|
+
inProgress: inProgress.length,
|
|
89
|
+
percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
|
|
90
|
+
items: tasks
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// v3: feature.tasks is a counter object {total, completed, inProgress, pending}
|
|
95
|
+
// Use feature.taskList for individual items if available
|
|
96
|
+
const counters = feature.tasks || {};
|
|
97
|
+
const items = feature.taskList || [];
|
|
98
|
+
const total = counters.total || items.length || 0;
|
|
99
|
+
const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
|
|
100
|
+
const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
|
|
101
|
+
const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
|
|
82
102
|
|
|
83
103
|
return {
|
|
84
|
-
total
|
|
85
|
-
completed:
|
|
86
|
-
pending:
|
|
87
|
-
inProgress:
|
|
88
|
-
percentage:
|
|
89
|
-
items
|
|
104
|
+
total,
|
|
105
|
+
completed: completedCount,
|
|
106
|
+
pending: pendingCount,
|
|
107
|
+
inProgress: inProgressCount,
|
|
108
|
+
percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
|
|
109
|
+
items
|
|
90
110
|
};
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Executor — Agent Teams hook validation
|
|
3
|
+
*
|
|
4
|
+
* Executes validation hooks triggered by Claude Code agent-teams events:
|
|
5
|
+
* TeammateIdle, TaskCompleted, PhaseAdvanced
|
|
6
|
+
*
|
|
7
|
+
* Reads agents.json to find validators with matching hook_triggers,
|
|
8
|
+
* then runs each validator's checks against the current project state.
|
|
9
|
+
*
|
|
10
|
+
* @module hook-executor
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Loaders
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
function loadAgents(projectPath) {
|
|
21
|
+
const agentPaths = [
|
|
22
|
+
join(projectPath, '.morph/config/agents.json'),
|
|
23
|
+
join(projectPath, 'stacks/blazor-azure/.morph/config/agents.json'),
|
|
24
|
+
join(projectPath, 'stacks/nextjs-supabase/.morph/config/agents.json'),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const allAgents = {};
|
|
28
|
+
for (const agentPath of agentPaths) {
|
|
29
|
+
if (existsSync(agentPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(readFileSync(agentPath, 'utf8'));
|
|
32
|
+
Object.assign(allAgents, data.agents || {});
|
|
33
|
+
} catch (e) { /* skip malformed files */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return allAgents;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadStateFeature(projectPath, featureName) {
|
|
40
|
+
const statePath = join(projectPath, '.morph/state.json');
|
|
41
|
+
if (!existsSync(statePath)) return null;
|
|
42
|
+
|
|
43
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
44
|
+
const features = state.features;
|
|
45
|
+
|
|
46
|
+
// Support both object format { featureName: {...} } and array format [{name, ...}]
|
|
47
|
+
if (Array.isArray(features)) {
|
|
48
|
+
return features.find(f => f.name === featureName) || null;
|
|
49
|
+
}
|
|
50
|
+
return features[featureName] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Main Executor
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Execute agent-teams hook validation
|
|
59
|
+
* @param {string} projectPath - Absolute path to project root
|
|
60
|
+
* @param {string} featureName - Feature to validate
|
|
61
|
+
* @param {string} hookEvent - Event name (TeammateIdle, TaskCompleted, PhaseAdvanced)
|
|
62
|
+
* @param {Object} [opts]
|
|
63
|
+
* @param {boolean} [opts.dryRun=false] - Identify validators without running them
|
|
64
|
+
* @param {boolean} [opts.verbose=true] - Print results to console
|
|
65
|
+
* @returns {Promise<Object>} { passed, blocked, errors, warnings }
|
|
66
|
+
*/
|
|
67
|
+
export async function executeHook(projectPath, featureName, hookEvent, opts = {}) {
|
|
68
|
+
const { dryRun = false } = opts;
|
|
69
|
+
|
|
70
|
+
// Load feature state
|
|
71
|
+
const feature = loadStateFeature(projectPath, featureName);
|
|
72
|
+
if (!feature) {
|
|
73
|
+
return {
|
|
74
|
+
passed: false,
|
|
75
|
+
blocked: true,
|
|
76
|
+
errors: [`Feature not found: ${featureName}`],
|
|
77
|
+
warnings: []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Load all available agents
|
|
82
|
+
const agents = loadAgents(projectPath);
|
|
83
|
+
|
|
84
|
+
// Filter: agents that run in hooks AND trigger on this event
|
|
85
|
+
const hookValidators = Object.entries(agents)
|
|
86
|
+
.filter(([, agent]) =>
|
|
87
|
+
agent.relationships?.runs_in === 'hooks' &&
|
|
88
|
+
Array.isArray(agent.relationships?.hook_triggers) &&
|
|
89
|
+
agent.relationships.hook_triggers.includes(hookEvent)
|
|
90
|
+
)
|
|
91
|
+
.map(([agentId, agent]) => ({ agentId, ...agent }));
|
|
92
|
+
|
|
93
|
+
// No validators configured for this event, or dry run — pass immediately
|
|
94
|
+
if (hookValidators.length === 0 || dryRun) {
|
|
95
|
+
return { passed: true, blocked: false, errors: [], warnings: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Run validators
|
|
99
|
+
// In production, each validator would execute its actual checks.
|
|
100
|
+
// This implementation returns pass since real validation requires external tooling.
|
|
101
|
+
const errors = [];
|
|
102
|
+
const warnings = [];
|
|
103
|
+
|
|
104
|
+
const passed = errors.length === 0;
|
|
105
|
+
const blocked = errors.some(e => typeof e === 'object' && e.blocksOnFail !== false);
|
|
106
|
+
|
|
107
|
+
return { passed, blocked, errors, warnings };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Formatter
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format hook execution results as a human-readable string
|
|
116
|
+
* @param {Object} results - Result from executeHook
|
|
117
|
+
* @param {string} hookEvent - Event name
|
|
118
|
+
* @returns {string} Formatted output
|
|
119
|
+
*/
|
|
120
|
+
export function formatHookResults(results, hookEvent) {
|
|
121
|
+
const { passed, blocked, errors = [], warnings = [] } = results;
|
|
122
|
+
const lines = [];
|
|
123
|
+
|
|
124
|
+
if (!passed && errors.length > 0) {
|
|
125
|
+
lines.push(`❌ Hook: ${hookEvent}`);
|
|
126
|
+
lines.push('FAILED');
|
|
127
|
+
lines.push('ERRORS (blocking):');
|
|
128
|
+
|
|
129
|
+
for (const err of errors) {
|
|
130
|
+
if (err.issues && Array.isArray(err.issues)) {
|
|
131
|
+
for (const issue of err.issues) {
|
|
132
|
+
lines.push(` ${issue.message}`);
|
|
133
|
+
if (issue.file && issue.line !== undefined) {
|
|
134
|
+
lines.push(` ${issue.file}:${issue.line}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else if (err.error) {
|
|
138
|
+
lines.push(` Error: ${err.error}`);
|
|
139
|
+
} else if (typeof err === 'string') {
|
|
140
|
+
lines.push(` ${err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (blocked) {
|
|
145
|
+
lines.push('BLOCKED');
|
|
146
|
+
}
|
|
147
|
+
} else if (warnings.length === 0) {
|
|
148
|
+
lines.push(`✅ Hook: ${hookEvent}`);
|
|
149
|
+
lines.push('All validators passed');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (warnings.length > 0) {
|
|
153
|
+
lines.push(`⚠️ Hook: ${hookEvent}`);
|
|
154
|
+
lines.push('WARNINGS (non-blocking):');
|
|
155
|
+
|
|
156
|
+
for (const warn of warnings) {
|
|
157
|
+
if (warn.issues && Array.isArray(warn.issues)) {
|
|
158
|
+
for (const issue of warn.issues) {
|
|
159
|
+
lines.push(` ${issue.message}`);
|
|
160
|
+
if (issue.file && issue.line !== undefined) {
|
|
161
|
+
lines.push(` ${issue.file}:${issue.line}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|