@polymorphism-tech/morph-spec 4.10.6 → 4.10.8

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 (149) hide show
  1. package/CLAUDE.md +3 -0
  2. package/LICENSE +71 -71
  3. package/README.md +379 -379
  4. package/bin/morph-spec.js +89 -0
  5. package/bin/task-manager.js +59 -6
  6. package/claude-plugin.json +14 -14
  7. package/docs/CHEATSHEET.md +1 -1
  8. package/docs/QUICKSTART.md +206 -206
  9. package/framework/agents/README.md +14 -14
  10. package/framework/agents/architecture/standards-architect.md +159 -159
  11. package/framework/agents/frontend/blazor-builder.md +13 -0
  12. package/framework/agents/frontend/css-specialist.md +87 -0
  13. package/framework/agents/frontend/nextjs-expert.md +13 -0
  14. package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
  15. package/framework/agents/frontend/ui-ux-designer.md +80 -10
  16. package/framework/agents/infrastructure/azure-architect.md +149 -149
  17. package/framework/agents/infrastructure/infra-architect.md +48 -48
  18. package/framework/agents.json +2268 -2567
  19. package/framework/hooks/README.md +204 -204
  20. package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
  21. package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
  22. package/framework/hooks/shared/phase-utils.js +139 -139
  23. package/framework/hooks/shared/worktree-helpers.js +30 -8
  24. package/framework/rules/csharp-standards.md +13 -13
  25. package/framework/rules/frontend-standards.md +13 -13
  26. package/framework/rules/infrastructure-standards.md +16 -16
  27. package/framework/rules/testing-standards.md +14 -14
  28. package/framework/skills/README.md +0 -1
  29. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
  30. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  31. package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
  32. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
  33. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
  34. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
  35. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
  36. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
  37. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
  38. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
  39. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
  40. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  41. package/framework/standards/STANDARDS.json +2040 -2040
  42. package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
  43. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  44. package/framework/templates/README.md +814 -814
  45. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  46. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  47. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  48. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  49. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  50. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  51. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  52. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  53. package/framework/templates/code/dotnet/test.cs +239 -239
  54. package/framework/templates/code/sql/rls-policy.sql +57 -57
  55. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  56. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  57. package/framework/templates/code/typescript/contracts.ts +168 -168
  58. package/framework/templates/context/CONTEXT.md +181 -181
  59. package/framework/templates/context/design-system.md +211 -0
  60. package/framework/templates/docs/proposal.md +182 -182
  61. package/framework/templates/docs/user-stories.md +34 -34
  62. package/framework/templates/examples/design-system-examples.md +357 -357
  63. package/framework/templates/examples/spec-examples.md +90 -90
  64. package/framework/templates/feature/decisions.md +187 -187
  65. package/framework/templates/feature/recap.md +146 -146
  66. package/framework/templates/feature/tasks.md +199 -199
  67. package/framework/templates/infrastructure/azure/README.md +286 -286
  68. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  69. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  70. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  71. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  72. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  73. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  74. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  75. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  76. package/package.json +82 -82
  77. package/src/commands/agents/agent-metrics.js +174 -0
  78. package/src/commands/agents/audit-dispatch.js +82 -0
  79. package/src/commands/agents/detect-agents.js +275 -0
  80. package/src/commands/agents/dispatch-agents.js +41 -135
  81. package/src/commands/agents/index.js +13 -4
  82. package/src/commands/agents/manage-agents.js +352 -0
  83. package/src/commands/project/doctor.js +59 -0
  84. package/src/commands/project/install-plugin-cmd.js +28 -28
  85. package/src/commands/project/setup-infra-cmd.js +12 -12
  86. package/src/commands/project/update.js +1 -17
  87. package/src/commands/project/worktree.js +58 -2
  88. package/src/commands/state/advance-phase.js +11 -3
  89. package/src/commands/tasks/index.js +4 -4
  90. package/src/commands/templates/index.js +5 -5
  91. package/src/commands/utils/index.js +3 -3
  92. package/src/commands/validation/index.js +4 -4
  93. package/src/core/state/phase-state-machine.js +217 -217
  94. package/src/core/state/state-manager.js +81 -0
  95. package/src/lib/agents/agents-manager.js +349 -0
  96. package/src/lib/agents/metrics-calculator.js +125 -0
  97. package/src/lib/agents/output-collector.js +75 -0
  98. package/src/lib/generators/context-generator.js +8 -7
  99. package/src/lib/generators/design-system-generator.js +298 -298
  100. package/src/lib/generators/index.js +10 -10
  101. package/src/lib/generators/metadata-extractor.js +387 -387
  102. package/src/lib/stack-filter.js +65 -65
  103. package/src/lib/standards/index.js +7 -7
  104. package/src/lib/standards/standards-context-injector.js +302 -302
  105. package/src/lib/tasks/task-parser.js +94 -3
  106. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  107. package/src/lib/validators/architecture/index.js +7 -7
  108. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  109. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  110. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  111. package/src/lib/validators/blazor/index.js +9 -9
  112. package/src/lib/validators/checkpoint-persistence.js +39 -0
  113. package/src/lib/validators/content/content-validator.js +287 -279
  114. package/src/lib/validators/content/index.js +7 -7
  115. package/src/lib/validators/contracts/index.js +7 -7
  116. package/src/lib/validators/css/css-validator.js +352 -352
  117. package/src/lib/validators/css/index.js +7 -7
  118. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  119. package/src/lib/validators/design-system/index.js +7 -7
  120. package/src/lib/validators/nextjs/index.js +6 -6
  121. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  122. package/src/lib/validators/packages/index.js +7 -7
  123. package/src/lib/validators/packages/package-validator.js +360 -360
  124. package/src/lib/validators/shared/index.js +12 -12
  125. package/src/lib/validators/shared/issue-counter.js +18 -18
  126. package/src/lib/validators/shared/result-formatter.js +124 -124
  127. package/src/lib/validators/spec-validator.js +2 -2
  128. package/src/lib/validators/ui/index.js +7 -7
  129. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  130. package/src/lib/validators/validation-runner.js +20 -0
  131. package/src/scripts/global-install.js +58 -58
  132. package/src/scripts/install-plugin.js +126 -126
  133. package/src/scripts/setup-infra.js +1 -17
  134. package/src/utils/agent-id-mapper.js +11 -0
  135. package/src/utils/agents-installer.js +40 -8
  136. package/src/utils/claude-md-injector.js +90 -90
  137. package/src/utils/claude-settings-manager.js +1 -1
  138. package/src/utils/file-copier.js +200 -196
  139. package/src/utils/hooks-installer.js +556 -739
  140. package/src/utils/logger.js +32 -32
  141. package/src/utils/skills-installer.js +178 -178
  142. package/src/utils/tier-helpers.js +24 -0
  143. package/src/utils/version-checker.js +175 -175
  144. package/framework/agents/architecture/po-pm-advisor.md +0 -203
  145. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
  146. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  147. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  148. package/framework/templates/.idea/morph-templates.xml +0 -92
  149. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -0,0 +1,349 @@
1
+ /**
2
+ * agents-manager.js — Core logic for agent CRUD operations
3
+ *
4
+ * Pure functions that accept agentsData object. File I/O only in
5
+ * loadAgentsFile / saveAgentsFile. All other functions are testable
6
+ * with fixture data.
7
+ *
8
+ * @module agents-manager
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
12
+ import { mkdirSync } from 'node:fs';
13
+ import { dirname } from 'node:path';
14
+
15
+ // ── Constants ──────────────────────────────────────────────────────────────────
16
+
17
+ /** Known valid phases for active_phases validation */
18
+ export const KNOWN_PHASES = [
19
+ 'proposal', 'setup', 'uiux', 'design',
20
+ 'clarify', 'plan', 'tasks', 'implement',
21
+ ];
22
+
23
+ /** Tier labels for display and validation */
24
+ export const TIER_LABELS = {
25
+ 1: 'orchestrator',
26
+ 2: 'leader',
27
+ 3: 'specialist',
28
+ 4: 'validator',
29
+ };
30
+
31
+ /** Tier metadata key names in agents.json */
32
+ export const TIER_KEYS = {
33
+ 1: 'tier_1_orchestrators',
34
+ 2: 'tier_2_leaders',
35
+ 3: 'tier_3_specialists',
36
+ 4: 'tier_4_validators',
37
+ };
38
+
39
+ /** Agent ID pattern: kebab-case starting with a letter */
40
+ export const AGENT_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
41
+
42
+ // ── Validation ─────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Validate agent fields.
46
+ * @param {string} id
47
+ * @param {object} fields
48
+ * @param {string[]} existingIds - IDs already in agents.json (for uniqueness check)
49
+ * @param {string[]} [knownGroups] - Known group names (for warnings)
50
+ * @param {object} [allAgents] - Full agents map (for circular reports_to check)
51
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
52
+ */
53
+ export function validateAgent(id, fields, existingIds = [], knownGroups = [], allAgents = {}) {
54
+ const errors = [];
55
+ const warnings = [];
56
+
57
+ // ID validation
58
+ if (!AGENT_ID_PATTERN.test(id)) {
59
+ errors.push('Agent ID must be kebab-case (lowercase letters, numbers, hyphens) and start with a letter');
60
+ }
61
+ if (existingIds.includes(id)) {
62
+ errors.push(`Agent ID '${id}' is not unique — already exists in agents.json`);
63
+ }
64
+
65
+ // Tier
66
+ const tier = fields.tier;
67
+ if (tier == null || !Number.isInteger(tier) || tier < 1 || tier > 4) {
68
+ errors.push('Tier must be 1 (orchestrator), 2 (leader), 3 (specialist), or 4 (validator)');
69
+ }
70
+
71
+ // Title
72
+ if (!fields.title || typeof fields.title !== 'string' || fields.title.trim() === '') {
73
+ errors.push('Title is required');
74
+ }
75
+
76
+ // Group
77
+ if (!fields.group || typeof fields.group !== 'string' || fields.group.trim() === '') {
78
+ errors.push('Group is required');
79
+ } else if (knownGroups.length > 0 && !knownGroups.includes(fields.group)) {
80
+ warnings.push(`Unknown group '${fields.group}' — known groups: ${knownGroups.join(', ')}`);
81
+ }
82
+
83
+ // Active phases
84
+ if (fields.active_phases) {
85
+ const phases = fields.active_phases;
86
+ if (!Array.isArray(phases)) {
87
+ errors.push('active_phases must be an array');
88
+ } else if (phases.length === 1 && phases[0] === '*') {
89
+ // wildcard — valid
90
+ } else {
91
+ for (const p of phases) {
92
+ if (!KNOWN_PHASES.includes(p)) {
93
+ errors.push(`Invalid phase '${p}' — valid: ${KNOWN_PHASES.join(', ')}`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // reports_to — circular check
100
+ if (fields.reports_to) {
101
+ if (fields.reports_to === id) {
102
+ errors.push('Agent cannot report to itself');
103
+ } else if (fields.reports_to !== 'user' && allAgents && Object.keys(allAgents).length > 0) {
104
+ // Walk the chain to detect cycles
105
+ const visited = new Set([id]);
106
+ let current = fields.reports_to;
107
+ while (current && current !== 'user') {
108
+ if (visited.has(current)) {
109
+ errors.push(`Circular reports_to chain detected: ${[...visited, current].join(' \u2192 ')}`);
110
+ break;
111
+ }
112
+ visited.add(current);
113
+ current = allAgents[current]?.reports_to;
114
+ }
115
+ }
116
+ }
117
+
118
+ // spawn_prompt required for tier >= 3
119
+ if (Number.isInteger(tier) && tier >= 3) {
120
+ const hasPrompt = fields.teammate?.spawn_prompt ||
121
+ (typeof fields.spawn_prompt === 'string' && fields.spawn_prompt.trim());
122
+ if (!hasPrompt) {
123
+ errors.push('spawn_prompt is required for tier 3+ agents');
124
+ }
125
+ }
126
+
127
+ return { valid: errors.length === 0, errors, warnings };
128
+ }
129
+
130
+ // ── Metadata Sync ──────────────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Recalculate total_agents and tier counts.
134
+ * @param {object} agentsData - Full agents.json data
135
+ * @returns {object} Updated agentsData (mutated in place + returned)
136
+ */
137
+ export function syncMetadata(agentsData) {
138
+ const agents = agentsData.agents || {};
139
+ const entries = Object.values(agents);
140
+
141
+ agentsData.total_agents = entries.length;
142
+ agentsData.tiers = {
143
+ tier_1_orchestrators: entries.filter(a => a.tier === 1).length,
144
+ tier_2_leaders: entries.filter(a => a.tier === 2).length,
145
+ tier_3_specialists: entries.filter(a => a.tier === 3).length,
146
+ tier_4_validators: entries.filter(a => a.tier === 4).length,
147
+ };
148
+
149
+ return agentsData;
150
+ }
151
+
152
+ // ── List + Show ────────────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * List agents with optional filters.
156
+ * @param {object} agentsData
157
+ * @param {{ group?: string, tier?: number }} [filters]
158
+ * @returns {{ agents: Array, groupCounts: Record<string, number>, total: number }}
159
+ */
160
+ export function listAgents(agentsData, filters = {}) {
161
+ const agents = agentsData.agents || {};
162
+ let entries = Object.entries(agents).map(([id, data]) => ({ id, ...data }));
163
+
164
+ if (filters.group) {
165
+ entries = entries.filter(a => a.group === filters.group);
166
+ }
167
+ if (filters.tier != null) {
168
+ entries = entries.filter(a => a.tier === filters.tier);
169
+ }
170
+
171
+ entries.sort((a, b) => a.tier - b.tier || a.id.localeCompare(b.id));
172
+
173
+ // groupCounts over ALL agents (not filtered)
174
+ const groupCounts = {};
175
+ for (const a of Object.values(agents)) {
176
+ groupCounts[a.group] = (groupCounts[a.group] || 0) + 1;
177
+ }
178
+
179
+ return {
180
+ agents: entries.map(a => ({
181
+ id: a.id,
182
+ tier: a.tier,
183
+ title: a.title,
184
+ group: a.group,
185
+ phases: Array.isArray(a.active_phases) ? a.active_phases.join(',') : '*',
186
+ keywords: (a.keywords || []).join(', ') || '\u2014',
187
+ })),
188
+ groupCounts,
189
+ total: entries.length,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Show detailed info for a single agent.
195
+ * @param {object} agentsData
196
+ * @param {string} id
197
+ * @returns {object|null}
198
+ */
199
+ export function showAgent(agentsData, id) {
200
+ return agentsData.agents?.[id] ?? null;
201
+ }
202
+
203
+ // ── Mutations ──────────────────────────────────────────────────────────────────
204
+
205
+ /**
206
+ * Add a new agent.
207
+ * @param {object} agentsData
208
+ * @param {string} id
209
+ * @param {object} fields
210
+ * @returns {{ success: boolean, data?: object, changes?: string[], errors?: string[], warnings?: string[] }}
211
+ */
212
+ export function addAgent(agentsData, id, fields) {
213
+ const existingIds = Object.keys(agentsData.agents || {});
214
+ const knownGroups = [...new Set(Object.values(agentsData.agents || {}).map(a => a.group))];
215
+ const validation = validateAgent(id, fields, existingIds, knownGroups, agentsData.agents);
216
+
217
+ if (!validation.valid) {
218
+ return { success: false, errors: validation.errors, warnings: validation.warnings };
219
+ }
220
+
221
+ const entry = {
222
+ tier: fields.tier,
223
+ title: fields.title,
224
+ group: fields.group,
225
+ active_phases: fields.active_phases || ['implement'],
226
+ reports_to: fields.reports_to || 'user',
227
+ domains: fields.domains || [],
228
+ keywords: fields.keywords || [],
229
+ validators: fields.validators || [],
230
+ standards: fields.standards || [],
231
+ };
232
+
233
+ if (fields.stacks) entry.stacks = fields.stacks;
234
+ if (fields.teammate) entry.teammate = fields.teammate;
235
+
236
+ agentsData.agents[id] = entry;
237
+ syncMetadata(agentsData);
238
+
239
+ return {
240
+ success: true,
241
+ data: agentsData,
242
+ changes: [`Added agent '${id}' (tier ${fields.tier}, group '${fields.group}')`],
243
+ warnings: validation.warnings,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Edit existing agent fields.
249
+ * @param {object} agentsData
250
+ * @param {string} id
251
+ * @param {object} changes
252
+ * @returns {{ success: boolean, data?: object, changes?: string[], errors?: string[] }}
253
+ */
254
+ export function editAgent(agentsData, id, changes) {
255
+ const agent = agentsData.agents?.[id];
256
+ if (!agent) {
257
+ return { success: false, errors: [`Agent '${id}' not found`] };
258
+ }
259
+
260
+ const changedFields = [];
261
+
262
+ // Handle spawn_prompt shortcut -> nested teammate
263
+ if (changes.spawn_prompt) {
264
+ if (!agent.teammate) {
265
+ agent.teammate = { role: 'specialist', icon: '\u{1F527}', spawn_prompt: changes.spawn_prompt };
266
+ } else {
267
+ agent.teammate.spawn_prompt = changes.spawn_prompt;
268
+ }
269
+ changedFields.push('teammate.spawn_prompt');
270
+ delete changes.spawn_prompt;
271
+ }
272
+
273
+ // Apply direct field changes
274
+ for (const [key, value] of Object.entries(changes)) {
275
+ if (value !== undefined && agent[key] !== value) {
276
+ changedFields.push(key);
277
+ agent[key] = value;
278
+ }
279
+ }
280
+
281
+ if (changedFields.length === 0) {
282
+ return { success: true, data: agentsData, changes: ['No changes detected'] };
283
+ }
284
+
285
+ syncMetadata(agentsData);
286
+ return { success: true, data: agentsData, changes: changedFields.map(f => `${f} updated`) };
287
+ }
288
+
289
+ /**
290
+ * Remove an agent.
291
+ * @param {object} agentsData
292
+ * @param {string} id
293
+ * @returns {{ success: boolean, data?: object, dependents?: string[], errors?: string[] }}
294
+ */
295
+ export function removeAgent(agentsData, id) {
296
+ if (!agentsData.agents?.[id]) {
297
+ return { success: false, errors: [`Agent '${id}' not found`] };
298
+ }
299
+
300
+ // Find dependents
301
+ const dependents = Object.entries(agentsData.agents)
302
+ .filter(([aid, a]) => aid !== id && a.reports_to === id)
303
+ .map(([aid]) => aid);
304
+
305
+ delete agentsData.agents[id];
306
+ syncMetadata(agentsData);
307
+
308
+ return { success: true, data: agentsData, dependents };
309
+ }
310
+
311
+ // ── File I/O ───────────────────────────────────────────────────────────────────
312
+
313
+ /**
314
+ * Load and parse agents.json.
315
+ * @param {string} filePath
316
+ * @returns {object}
317
+ * @throws {Error} If file not found or invalid JSON
318
+ */
319
+ export function loadAgentsFile(filePath) {
320
+ if (!existsSync(filePath)) {
321
+ throw new Error(`agents.json not found at ${filePath}. Run 'morph-spec doctor' to diagnose.`);
322
+ }
323
+
324
+ try {
325
+ const content = readFileSync(filePath, 'utf8');
326
+ return JSON.parse(content);
327
+ } catch (err) {
328
+ if (err instanceof SyntaxError) {
329
+ throw new Error(`Failed to parse agents.json: ${err.message}. Run 'morph-spec doctor' to diagnose.`);
330
+ }
331
+ throw err;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Write agents.json atomically (write to .tmp, then rename).
337
+ * @param {string} filePath
338
+ * @param {object} agentsData
339
+ */
340
+ export function saveAgentsFile(filePath, agentsData) {
341
+ const dir = dirname(filePath);
342
+ if (!existsSync(dir)) {
343
+ mkdirSync(dir, { recursive: true });
344
+ }
345
+
346
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
347
+ writeFileSync(tmpPath, JSON.stringify(agentsData, null, 2) + '\n', 'utf8');
348
+ renameSync(tmpPath, filePath);
349
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Agent Metrics Calculator — Pure computation, no I/O.
3
+ *
4
+ * Calculates per-agent performance metrics from dispatch log entries.
5
+ */
6
+
7
+ /**
8
+ * Calculate per-agent metrics from dispatch log entries.
9
+ * @param {Array} dispatchLog - Array of dispatch entries
10
+ * @returns {{ agents: Array, summary: Object }}
11
+ */
12
+ export function calculateMetrics(dispatchLog) {
13
+ if (!Array.isArray(dispatchLog) || dispatchLog.length === 0) {
14
+ return {
15
+ agents: [],
16
+ summary: { totalDispatches: 0, totalTasks: 0, completedTasks: 0, completionRate: 0, avgTaskDurationMs: null }
17
+ };
18
+ }
19
+
20
+ const agentMap = new Map();
21
+
22
+ for (const entry of dispatchLog) {
23
+ for (const agent of entry.agents || []) {
24
+ if (!agentMap.has(agent.id)) {
25
+ agentMap.set(agent.id, { dispatches: 0, tasksAssigned: 0, tasksCompleted: 0, durations: [] });
26
+ }
27
+ const acc = agentMap.get(agent.id);
28
+ acc.dispatches += 1;
29
+ acc.tasksAssigned += (agent.tasks || []).length;
30
+ acc.tasksCompleted += (agent.tasksCompleted || []).length;
31
+
32
+ if (agent.startedAt && agent.completedAt) {
33
+ const start = new Date(agent.startedAt).getTime();
34
+ const end = new Date(agent.completedAt).getTime();
35
+ if (!isNaN(start) && !isNaN(end) && end >= start) {
36
+ acc.durations.push(end - start);
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ const agents = [];
43
+ let totalDurationAll = 0;
44
+ let agentsWithDuration = 0;
45
+
46
+ for (const [id, acc] of agentMap) {
47
+ const totalDurationMs = acc.durations.length > 0
48
+ ? acc.durations.reduce((s, d) => s + d, 0)
49
+ : null;
50
+ const avgDurationMs = acc.durations.length > 0
51
+ ? Math.round(totalDurationMs / acc.durations.length)
52
+ : null;
53
+
54
+ if (totalDurationMs !== null) {
55
+ totalDurationAll += totalDurationMs;
56
+ agentsWithDuration++;
57
+ }
58
+
59
+ agents.push({
60
+ id,
61
+ dispatches: acc.dispatches,
62
+ tasksAssigned: acc.tasksAssigned,
63
+ tasksCompleted: acc.tasksCompleted,
64
+ completionRate: acc.tasksAssigned > 0 ? acc.tasksCompleted / acc.tasksAssigned : 0,
65
+ avgDurationMs,
66
+ totalDurationMs
67
+ });
68
+ }
69
+
70
+ agents.sort((a, b) => b.tasksCompleted - a.tasksCompleted);
71
+
72
+ const totalTasks = agents.reduce((s, a) => s + a.tasksAssigned, 0);
73
+ const completedTasks = agents.reduce((s, a) => s + a.tasksCompleted, 0);
74
+
75
+ return {
76
+ agents,
77
+ summary: {
78
+ totalDispatches: dispatchLog.length,
79
+ totalTasks,
80
+ completedTasks,
81
+ completionRate: totalTasks > 0 ? completedTasks / totalTasks : 0,
82
+ avgTaskDurationMs: completedTasks > 0 && totalDurationAll > 0
83
+ ? Math.round(totalDurationAll / completedTasks)
84
+ : null
85
+ }
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Format milliseconds as human-readable duration.
91
+ * @param {number|null} ms
92
+ * @returns {string}
93
+ */
94
+ export function formatDuration(ms) {
95
+ if (ms == null || ms < 0) return '\u2014';
96
+ if (ms === 0) return '0s';
97
+
98
+ const totalSeconds = Math.floor(ms / 1000);
99
+ const hours = Math.floor(totalSeconds / 3600);
100
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
101
+ const seconds = totalSeconds % 60;
102
+
103
+ if (hours > 0) return `${hours}h ${minutes}m`;
104
+ if (minutes > 0) return `${minutes}m`;
105
+ return `${seconds}s`;
106
+ }
107
+
108
+ /**
109
+ * Calculate cross-feature aggregated metrics.
110
+ * @param {Object} features - state.features object
111
+ * @returns {{ agents: Array, summary: Object, featureCount: number }}
112
+ */
113
+ export function calculateAggregatedMetrics(features) {
114
+ const allEntries = [];
115
+ let featureCount = 0;
116
+
117
+ for (const feature of Object.values(features || {})) {
118
+ featureCount++;
119
+ const log = feature.dispatchLog || [];
120
+ allEntries.push(...log);
121
+ }
122
+
123
+ const result = calculateMetrics(allEntries);
124
+ return { ...result, featureCount };
125
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * output-collector — Collect phase output files for multi-source agent detection
3
+ *
4
+ * Reads all existing phase outputs for a feature and returns them as
5
+ * weighted TextSource[] for use in detectAgents().
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { OUTPUT_SCHEMA, getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
10
+
11
+ /**
12
+ * Default source weights per output type.
13
+ * Keys match OUTPUT_SCHEMA keys (camelCase).
14
+ */
15
+ export const SOURCE_WEIGHTS = {
16
+ proposal: 1.0,
17
+ schemaAnalysis: 1.0,
18
+ spec: 1.5,
19
+ contracts: 1.0,
20
+ contractsTs: 1.0,
21
+ contractsVsa: 1.0,
22
+ clarifications: 1.0,
23
+ decisions: 1.0,
24
+ plan: 1.2,
25
+ tasks: 1.3,
26
+ uiDesignSystem: 1.0,
27
+ uiMockups: 1.0,
28
+ uiComponents: 1.0,
29
+ uiFlows: 1.0,
30
+ recap: 1.0,
31
+ };
32
+
33
+ /** Phase order for sorting collected outputs */
34
+ const PHASE_ORDER = ['proposal', 'uiux', 'design', 'clarify', 'plan', 'tasks', 'implement'];
35
+
36
+ /**
37
+ * Collect all existing phase output files for a feature.
38
+ *
39
+ * @param {string} projectRoot - Absolute path to project root
40
+ * @param {string} featureName - Feature name
41
+ * @returns {Array<{source: string, content: string, weight: number}>}
42
+ */
43
+ export function collectPhaseOutputs(projectRoot, featureName) {
44
+ const results = [];
45
+
46
+ for (const [key, entry] of Object.entries(OUTPUT_SCHEMA)) {
47
+ const absPath = getAbsoluteOutputPath(projectRoot, featureName, key);
48
+ if (!existsSync(absPath)) continue;
49
+
50
+ try {
51
+ const content = readFileSync(absPath, 'utf8');
52
+ if (!content.trim()) continue; // Skip empty files
53
+
54
+ results.push({
55
+ source: entry.filename,
56
+ content,
57
+ weight: SOURCE_WEIGHTS[key] ?? 1.0,
58
+ _phase: entry.phase,
59
+ });
60
+ } catch {
61
+ // Skip unreadable files — fail-open
62
+ continue;
63
+ }
64
+ }
65
+
66
+ // Sort by phase order
67
+ results.sort((a, b) => {
68
+ const orderA = PHASE_ORDER.indexOf(a._phase);
69
+ const orderB = PHASE_ORDER.indexOf(b._phase);
70
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
71
+ });
72
+
73
+ // Remove internal _phase field
74
+ return results.map(({ source, content, weight }) => ({ source, content, weight }));
75
+ }
@@ -15,6 +15,7 @@ import fs from 'fs/promises';
15
15
  import path from 'path';
16
16
  import { existsSync } from 'fs';
17
17
  import { getOutputPath } from '../../core/paths/output-schema.js';
18
+ import { tierToRole } from '../../utils/tier-helpers.js';
18
19
 
19
20
  /**
20
21
  * Resolve a template path: check .morph/framework/templates/ first, then framework/templates/.
@@ -259,7 +260,7 @@ export async function generateProjectContext(projectPath) {
259
260
  .map(([id, agent]) => ({
260
261
  id,
261
262
  title: agent.title,
262
- always_active: agent.always_active
263
+ always_active: Array.isArray(agent.active_phases) ? agent.active_phases.includes('*') : !!agent.always_active
263
264
  })),
264
265
  TIER_2_AGENTS: Object.entries(agents?.agents || {})
265
266
  .filter(([id, agent]) => !id.startsWith('_') && agent.tier === 2)
@@ -273,7 +274,7 @@ export async function generateProjectContext(projectPath) {
273
274
  .map(([id, agent]) => ({
274
275
  id,
275
276
  title: agent.title,
276
- reports_to: agent.relationships?.reports_to || 'N/A'
277
+ reports_to: agent.reports_to || agent.relationships?.reports_to || 'N/A'
277
278
  })),
278
279
 
279
280
  // Agent Teams config
@@ -381,10 +382,10 @@ export async function generateFeatureContext(projectPath, featureName) {
381
382
  .map(id => ({ id, ...agents.agents[id] }));
382
383
 
383
384
  // Group by tier and build squads
384
- const teamLead = activeAgents.find(a => a.tier === 1 && a.role === 'orchestrator');
385
- const domainLeaders = activeAgents.filter(a => a.tier === 2 && a.role === 'domain-leader');
386
- const specialists = activeAgents.filter(a => a.tier === 3 && a.role === 'specialist');
387
- const validators = activeAgents.filter(a => a.tier === 4 && a.role === 'validator');
385
+ const teamLead = activeAgents.find(a => tierToRole(a.tier) === 'orchestrator');
386
+ const domainLeaders = activeAgents.filter(a => tierToRole(a.tier) === 'leader');
387
+ const specialists = activeAgents.filter(a => tierToRole(a.tier) === 'specialist');
388
+ const validators = activeAgents.filter(a => tierToRole(a.tier) === 'validator');
388
389
 
389
390
  const squads = domainLeaders.map(leader => ({
390
391
  leader: {
@@ -392,7 +393,7 @@ export async function generateFeatureContext(projectPath, featureName) {
392
393
  title: leader.title
393
394
  },
394
395
  members: specialists
395
- .filter(s => s.relationships?.reports_to === leader.id)
396
+ .filter(s => (s.reports_to || s.relationships?.reports_to) === leader.id)
396
397
  .map(s => ({
397
398
  id: s.id,
398
399
  title: s.title