@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.
Files changed (140) hide show
  1. package/CLAUDE.md +108 -946
  2. package/bin/morph-spec.js +284 -9
  3. package/bin/task-manager.cjs +102 -14
  4. package/bin/validate.js +4 -4
  5. package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
  6. package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
  7. package/docs/next-generation/EXECUTION-FLOW.md +274 -0
  8. package/docs/next-generation/META-PROMPTS.md +235 -0
  9. package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
  10. package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
  11. package/package.json +5 -5
  12. package/src/commands/agents/agents-fuse.js +97 -0
  13. package/src/commands/agents/micro-agent.js +112 -0
  14. package/src/commands/agents/spawn-team.js +69 -4
  15. package/src/commands/agents/squad-template.js +146 -0
  16. package/src/commands/analytics/analytics.js +176 -0
  17. package/src/commands/context/context-prime.js +63 -0
  18. package/src/commands/context/core-four.js +54 -0
  19. package/src/commands/mcp/mcp.js +102 -0
  20. package/src/commands/project/detect-agents.js +32 -2
  21. package/src/commands/project/detect.js +11 -1
  22. package/src/commands/project/doctor.js +573 -356
  23. package/src/commands/project/init.js +9 -2
  24. package/src/commands/project/update.js +13 -3
  25. package/src/commands/state/advance-phase.js +448 -416
  26. package/src/commands/state/state.js +14 -12
  27. package/src/commands/tasks/task.js +1 -1
  28. package/src/commands/templates/template-render.js +80 -1
  29. package/src/commands/threads/thread-template.js +103 -0
  30. package/src/commands/threads/threads.js +261 -0
  31. package/src/commands/trust/trust.js +205 -0
  32. package/src/{orchestrator.js → core/orchestrator.js} +8 -8
  33. package/src/core/state/state-manager.js +37 -17
  34. package/src/core/workflows/workflow-detector.js +114 -3
  35. package/src/lib/agents/micro-agent-factory.js +161 -0
  36. package/src/lib/analytics/analytics-engine.js +345 -0
  37. package/src/lib/checkpoints/checkpoint-hooks.js +298 -258
  38. package/src/lib/context/context-bundler.js +240 -0
  39. package/src/lib/context/context-optimizer.js +212 -0
  40. package/src/lib/context/context-tracker.js +273 -0
  41. package/src/lib/context/core-four-tracker.js +201 -0
  42. package/src/lib/context/mcp-optimizer.js +200 -0
  43. package/src/lib/detectors/index.js +1 -1
  44. package/src/lib/detectors/standards-generator.js +77 -17
  45. package/src/lib/detectors/structure-detector.js +67 -39
  46. package/src/lib/execution/fusion-executor.js +304 -0
  47. package/src/lib/execution/parallel-executor.js +270 -0
  48. package/src/lib/generators/context-generator.js +3 -3
  49. package/src/lib/generators/recap-generator.js +32 -12
  50. package/src/lib/hooks/hook-executor.js +169 -0
  51. package/src/lib/hooks/stop-hook-executor.js +286 -0
  52. package/src/lib/hops/hop-composer.js +221 -0
  53. package/src/lib/threads/thread-coordinator.js +238 -0
  54. package/src/lib/threads/thread-manager.js +317 -0
  55. package/src/lib/tracking/artifact-trail.js +202 -0
  56. package/src/lib/trust/trust-manager.js +269 -0
  57. package/src/lib/validators/design-system/design-system-validator.js +2 -2
  58. package/src/lib/validators/validation-runner.js +14 -30
  59. package/src/utils/hooks-installer.js +69 -0
  60. package/stacks/blazor-azure/.morph/config/agents.json +72 -3
  61. package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
  62. package/docs/llm-interaction-config.md +0 -735
  63. package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
  64. package/src/commands/utils/migrate-state.js +0 -158
  65. package/src/commands/utils/upgrade.js +0 -346
  66. package/src/lib/validators/architecture-validator.js +0 -60
  67. package/src/lib/validators/content-validator.js +0 -164
  68. package/src/lib/validators/package-validator.js +0 -61
  69. package/src/lib/validators/ui-contrast-validator.js +0 -44
  70. package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
  71. package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
  72. package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
  73. package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
  74. package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
  75. package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
  76. package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
  77. package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
  78. package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
  79. package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
  80. package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
  81. package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
  82. package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
  83. package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
  84. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
  85. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
  86. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
  87. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
  88. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
  89. package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
  90. package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
  91. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
  92. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
  93. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
  94. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
  95. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
  96. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
  97. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
  98. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
  99. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
  100. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
  101. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
  102. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
  103. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
  104. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
  105. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
  106. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
  107. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
  108. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
  109. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
  110. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
  111. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
  112. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
  113. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
  114. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
  115. package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
  116. package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
  117. package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
  118. package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
  119. package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
  120. package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
  121. package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
  122. package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
  123. package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
  124. package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
  125. package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
  126. package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
  127. package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
  128. package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
  129. package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
  130. package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
  131. package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
  132. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
  133. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
  134. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
  135. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
  136. /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
  137. /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
  138. /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
  139. /package/docs/{v3.0 → next-generation}/README.md +0 -0
  140. /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('./stack-resolver.js');
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('./stack-resolver.js');
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('./stack-resolver.js');
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 './state-manager.js';
14
- import { runValidation } from './validation-runner.js';
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
- const tasks = Array.isArray(feature.tasks) ? feature.tasks : [];
79
- const completed = tasks.filter(t => t.status === 'completed');
80
- const pending = tasks.filter(t => t.status === 'pending');
81
- const inProgress = tasks.filter(t => t.status === 'in_progress');
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: tasks.length,
85
- completed: completed.length,
86
- pending: pending.length,
87
- inProgress: inProgress.length,
88
- percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
89
- items: tasks
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
+ }