@polymorphism-tech/morph-spec 4.8.12 → 4.8.15

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 (76) hide show
  1. package/README.md +379 -379
  2. package/bin/morph-spec.js +23 -2
  3. package/bin/{task-manager.cjs → task-manager.js} +249 -172
  4. package/claude-plugin.json +14 -14
  5. package/docs/CHEATSHEET.md +203 -203
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +224 -140
  8. package/framework/hooks/README.md +202 -202
  9. package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
  10. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
  11. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
  12. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
  13. package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
  14. package/framework/hooks/claude-code/statusline.py +6 -0
  15. package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
  16. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
  18. package/framework/hooks/shared/phase-utils.js +4 -1
  19. package/framework/hooks/shared/state-reader.js +1 -0
  20. package/framework/skills/README.md +1 -0
  21. package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
  22. package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
  23. package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
  24. package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
  25. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
  26. package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
  27. package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
  28. package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
  30. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
  31. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
  32. package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
  33. package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
  34. package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
  36. package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
  37. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
  38. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
  39. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
  40. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
  41. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  42. package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
  43. package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
  44. package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
  45. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
  46. package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
  47. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
  48. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
  49. package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
  50. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
  51. package/framework/squad-templates/backend-only.json +14 -1
  52. package/framework/squad-templates/frontend-only.json +14 -1
  53. package/framework/squad-templates/full-stack.json +25 -8
  54. package/framework/standards/STANDARDS.json +631 -86
  55. package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
  56. package/framework/templates/project/validate.js +122 -0
  57. package/framework/workflows/configs/zero-touch.json +7 -0
  58. package/package.json +87 -87
  59. package/src/commands/agents/dispatch-agents.js +53 -10
  60. package/src/commands/state/advance-phase.js +88 -13
  61. package/src/commands/state/index.js +2 -1
  62. package/src/commands/state/phase-runner.js +215 -0
  63. package/src/commands/tasks/task.js +25 -4
  64. package/src/core/paths/output-schema.js +2 -1
  65. package/src/lib/detectors/design-system-detector.js +5 -4
  66. package/src/lib/generators/recap-generator.js +16 -0
  67. package/src/lib/orchestration/team-orchestrator.js +171 -89
  68. package/src/lib/phase-chain/eligibility-checker.js +243 -0
  69. package/src/lib/standards/digest-builder.js +231 -0
  70. package/src/lib/tasks/task-parser.js +94 -0
  71. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
  72. package/src/lib/validators/content/content-validator.js +34 -106
  73. package/src/lib/validators/nextjs/next-component-validator.js +2 -0
  74. package/src/lib/validators/validation-runner.js +2 -2
  75. package/src/utils/file-copier.js +1 -0
  76. package/src/utils/hooks-installer.js +31 -7
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * @fileoverview Hierarchical Agent Teams Orchestrator
3
3
  *
4
- * Builds Agent Teams using 4-tier hierarchy:
5
- * - Tier 1: Orchestrators (Team Lead in delegate mode)
6
- * - Tier 2: Domain Leaders (Squad Leaders)
7
- * - Tier 3: Specialists (Domain experts)
8
- * - Tier 4: Validators (Run in hooks, not teammates)
4
+ * Builds Agent Teams using flattened 2-tier hierarchy:
5
+ * - Lead: standards-architect (in delegate mode, quality gate)
6
+ * - Teammates: Tier-2 domain leaders (each with specialists in their spawn prompt)
7
+ *
8
+ * NOTE: Agent Teams does NOT support nested teams (a teammate cannot spawn
9
+ * teammates). Therefore Tier-3 specialists are described in domain leader
10
+ * spawn prompts, not as separate teammates.
9
11
  *
10
12
  * @module team-orchestrator
11
13
  */
@@ -29,75 +31,87 @@ async function loadAgentsConfig(projectPath) {
29
31
  }
30
32
 
31
33
  /**
32
- * Detect if Agent Teams should be spawned based on complexity
34
+ * Detect whether Agent Teams, subagents, or a single session should be used.
35
+ *
36
+ * Returns one of three modes:
37
+ * - 'agent-teams': complexity=critical OR (multiDomain AND estimatedFiles>=15)
38
+ * - 'subagents': shouldDispatch=true but below agent-teams threshold
39
+ * - 'single': trivial/low complexity or no parallelization benefit
40
+ *
33
41
  * @param {Object} options
34
42
  * @param {string[]} options.activeAgents - Agent IDs detected for feature
35
43
  * @param {string} options.complexity - Complexity level (trivial|low|medium|high|critical)
36
44
  * @param {number} options.estimatedFiles - Estimated files to modify
37
45
  * @param {boolean} options.multiDomain - Does feature span multiple domains?
38
- * @returns {Object} { shouldSpawn: boolean, reason: string, recommendedMode: string }
46
+ * @returns {{ mode: 'single'|'subagents'|'agent-teams', reason: string }}
39
47
  */
40
48
  export function shouldSpawnAgentTeam(options) {
41
49
  const { activeAgents = [], complexity, estimatedFiles = 0, multiDomain = false } = options;
42
50
 
43
- // Never spawn for trivial/low complexity
51
+ // Never parallelize for trivial/low complexity
44
52
  if (complexity === 'trivial' || complexity === 'low') {
45
53
  return {
46
- shouldSpawn: false,
47
- reason: 'Complexity too low - single session sufficient',
48
- recommendedMode: 'single'
54
+ mode: 'single',
55
+ reason: 'Complexity too low single session sufficient',
49
56
  };
50
57
  }
51
58
 
52
- // Always spawn for critical complexity
59
+ // Agent Teams for critical complexity (max parallelization needed)
53
60
  if (complexity === 'critical') {
54
61
  return {
55
- shouldSpawn: true,
56
- reason: 'Critical complexity - parallel coordination essential',
57
- recommendedMode: 'tmux'
62
+ mode: 'agent-teams',
63
+ reason: 'Critical complexity Agent Teams with plan approval essential',
64
+ };
65
+ }
66
+
67
+ // Agent Teams for large multi-domain features (backend + frontend need contract coordination)
68
+ if (multiDomain && estimatedFiles >= 15) {
69
+ return {
70
+ mode: 'agent-teams',
71
+ reason: `Multi-domain feature with ${estimatedFiles}+ files — Agent Teams for contract coordination`,
58
72
  };
59
73
  }
60
74
 
61
- // Spawn if multi-domain (backend + frontend + infra)
75
+ // Subagents for multi-domain below agent-teams threshold
62
76
  if (multiDomain) {
63
77
  return {
64
- shouldSpawn: true,
65
- reason: 'Multi-domain feature - benefits from parallel squads',
66
- recommendedMode: 'auto'
78
+ mode: 'subagents',
79
+ reason: 'Multi-domain feature parallel subagents recommended',
67
80
  };
68
81
  }
69
82
 
70
- // Spawn if 3+ independent modules (high file count)
83
+ // Subagents for high file count
71
84
  if (estimatedFiles >= 15) {
72
85
  return {
73
- shouldSpawn: true,
74
- reason: 'High file count - parallel implementation recommended',
75
- recommendedMode: 'auto'
86
+ mode: 'subagents',
87
+ reason: `High file count (${estimatedFiles}) parallel subagents recommended`,
76
88
  };
77
89
  }
78
90
 
79
- // Spawn if 5+ specialists active (indicates complex coordination)
91
+ // Subagents when 5+ specialists are active
80
92
  if (activeAgents.length >= 5) {
81
93
  return {
82
- shouldSpawn: true,
83
- reason: '5+ specialists active - team coordination beneficial',
84
- recommendedMode: 'auto'
94
+ mode: 'subagents',
95
+ reason: `${activeAgents.length} specialists active subagents beneficial`,
85
96
  };
86
97
  }
87
98
 
88
99
  // Default: single session for medium complexity
89
100
  return {
90
- shouldSpawn: false,
91
- reason: 'Medium complexity - single session with subagents OK',
92
- recommendedMode: 'single'
101
+ mode: 'single',
102
+ reason: 'Medium complexity single session with subagents OK',
93
103
  };
94
104
  }
95
105
 
96
106
  /**
97
- * Build hierarchical team structure from active agents
107
+ * Build hierarchical team structure from active agents.
108
+ * Flattened to 2 tiers for Agent Teams compatibility:
109
+ * - Lead: standards-architect
110
+ * - Teammates: Tier-2 domain leaders (specialists are in their spawn prompts)
111
+ *
98
112
  * @param {string[]} activeAgentIds - Agent IDs to include in team
99
113
  * @param {Object} agentsConfig - Loaded agents.json
100
- * @returns {Object} Team structure with lead, domain leaders, specialists
114
+ * @returns {Object} Team structure with lead, domain leaders, specialists, validators
101
115
  */
102
116
  export function buildTeamHierarchy(activeAgentIds, agentsConfig) {
103
117
  const agents = agentsConfig.agents;
@@ -109,45 +123,112 @@ export function buildTeamHierarchy(activeAgentIds, agentsConfig) {
109
123
  const orchestrators = activeAgents.filter(a => a.tier === 1 && a.role === 'orchestrator');
110
124
  const teamLead = orchestrators.find(a => a.id === 'standards-architect') || orchestrators[0];
111
125
 
112
- // Tier 2: Domain Leaders
126
+ // Tier 2: Domain Leaders (direct teammates in Agent Teams)
113
127
  const domainLeaders = activeAgents.filter(a => a.tier === 2 && a.role === 'domain-leader');
114
128
 
115
- // Tier 3: Specialists (group by domain)
129
+ // Tier 3: Specialists (grouped under domain leaders, NOT separate teammates)
116
130
  const specialists = activeAgents.filter(a => a.tier === 3 && a.role === 'specialist');
117
131
 
118
132
  // Tier 4: Validators (run in hooks, NOT teammates)
119
133
  const validators = activeAgents.filter(a => a.tier === 4 && a.role === 'validator');
120
134
 
121
- // Group specialists under their Domain Leaders
135
+ // Group specialists under their Domain Leaders for spawn prompt inclusion
122
136
  const squads = {};
123
137
  domainLeaders.forEach(leader => {
124
138
  const squadMembers = specialists.filter(specialist => {
125
- // Check if specialist reports to this leader
126
139
  return specialist.relationships?.reports_to === leader.id;
127
140
  });
128
141
 
129
- if (squadMembers.length > 0) {
130
- squads[leader.id] = {
131
- leader,
132
- members: squadMembers
133
- };
134
- }
142
+ squads[leader.id] = {
143
+ leader,
144
+ members: squadMembers,
145
+ };
135
146
  });
136
147
 
137
148
  return {
138
149
  teamLead,
139
150
  squads,
140
- validators, // Not in teammates, run as hooks
141
- totalTeammates: 1 + domainLeaders.length + specialists.length // Lead + Leaders + Specialists
151
+ validators, // Run as hooks, not teammates
152
+ // Teammates = lead + domain leaders only (not tier-3 specialists)
153
+ totalTeammates: 1 + domainLeaders.length,
142
154
  };
143
155
  }
144
156
 
145
157
  /**
146
- * Generate teammate spawn instructions for Agent Teams
158
+ * Generate natural-language Agent Team creation prompt.
159
+ *
160
+ * Returns a string ready to be sent to Claude as instructions to create
161
+ * an agent team. Domain leaders are teammates; their tier-3 specialists
162
+ * are described within each leader's spawn context.
163
+ *
147
164
  * @param {Object} teamHierarchy - Output from buildTeamHierarchy
148
165
  * @param {string} featureName - Feature name
149
166
  * @param {string} phase - Current MORPH phase
150
- * @returns {Object[]} Array of teammate configs for Agent Teams
167
+ * @returns {string} Natural language team creation instructions
168
+ */
169
+ export function generateAgentTeamPrompt(teamHierarchy, featureName, phase) {
170
+ const { teamLead, squads } = teamHierarchy;
171
+ const squadEntries = Object.entries(squads);
172
+
173
+ if (squadEntries.length === 0) {
174
+ return `No multi-domain squad found for feature '${featureName}'. Use single session or subagents instead.`;
175
+ }
176
+
177
+ const teammateCount = squadEntries.length + 1; // domain leaders + quality lead
178
+ const lines = [
179
+ `Create an agent team to implement feature '${featureName}' with ${teammateCount} teammates:`,
180
+ '',
181
+ ];
182
+
183
+ for (const [leaderId, squad] of squadEntries) {
184
+ const leader = squad.leader;
185
+ const domain = leader.domains?.[0] || leaderId;
186
+ const specialistList = squad.members.length > 0
187
+ ? squad.members.map(m => m.title || m.id).join(', ')
188
+ : 'none';
189
+
190
+ lines.push(`- ${leader.title || leaderId} teammate (${leaderId}): Implement ${domain} tasks.`);
191
+ lines.push(` Context: spec.md, contracts.cs, standards/${domain}/`);
192
+ if (squad.members.length > 0) {
193
+ lines.push(` Specialists to coordinate (in your spawn prompt): ${specialistList}`);
194
+ }
195
+ lines.push(` Restriction: Modify ONLY files in the ${domain} domain.`);
196
+ lines.push('');
197
+ }
198
+
199
+ const leadId = teamLead?.id || 'standards-architect';
200
+ lines.push(`- Quality teammate (${leadId}): Review each teammate's output and run validate-feature.`);
201
+ lines.push(' Require plan approval before implementing. Only approve plans that match contracts.cs.');
202
+ lines.push('');
203
+ lines.push('Coordination protocol:');
204
+ lines.push(' - Backend must signal when contracts.cs/DTOs are stable before frontend implements API calls.');
205
+ lines.push(' - If teammates need to share interfaces, send a direct message to coordinate first.');
206
+ lines.push(' - Escalate contract conflicts to the quality teammate to resolve.');
207
+
208
+ return lines.join('\n');
209
+ }
210
+
211
+ /**
212
+ * Generate team instructions as natural language (renamed from generateSpawnCommand).
213
+ * Use this for Agent Teams mode.
214
+ *
215
+ * @param {Object} teamHierarchy - Output from buildTeamHierarchy
216
+ * @param {string} featureName - Feature name
217
+ * @param {string} phase - Current MORPH phase
218
+ * @returns {string} Natural language team creation instructions
219
+ */
220
+ export function generateTeamInstructions(teamHierarchy, featureName, phase) {
221
+ return generateAgentTeamPrompt(teamHierarchy, featureName, phase);
222
+ }
223
+
224
+ /**
225
+ * Generate teammate spawn configs for subagent dispatch (unchanged from v1).
226
+ * Use this for 'subagents' mode (not Agent Teams).
227
+ *
228
+ * @param {Object} teamHierarchy - Output from buildTeamHierarchy
229
+ * @param {string} featureName - Feature name
230
+ * @param {string} phase - Current MORPH phase
231
+ * @returns {Object[]} Array of teammate configs for Task tool subagents
151
232
  */
152
233
  export function generateTeammateConfigs(teamHierarchy, featureName, phase) {
153
234
  const teammates = [];
@@ -176,7 +257,6 @@ DO NOT implement code yourself. Coordinate Domain Leaders, resolve conflicts, sy
176
257
  Object.entries(squads).forEach(([leaderId, squad]) => {
177
258
  const leader = squad.leader;
178
259
 
179
- // Add Domain Leader
180
260
  if (leader.teammate) {
181
261
  teammates.push({
182
262
  id: leader.id,
@@ -195,7 +275,6 @@ Coordinate your specialists. Escalate conflicts to ${leader.relationships.escala
195
275
  });
196
276
  }
197
277
 
198
- // Add Specialists in this squad
199
278
  squad.members.forEach(specialist => {
200
279
  if (specialist.teammate) {
201
280
  teammates.push({
@@ -222,33 +301,22 @@ ${specialist.relationships.collaborates_with ?
222
301
  }
223
302
 
224
303
  /**
225
- * Generate Agent Teams command for spawning
226
- * @param {Object[]} teammates - Output from generateTeammateConfigs
227
- * @param {string} displayMode - 'auto' | 'in-process' | 'tmux'
228
- * @returns {string} Command to spawn Agent Teams
304
+ * @deprecated Use generateTeamInstructions() instead.
305
+ * Kept for backward compatibility will be removed in a future version.
229
306
  */
230
- export function generateSpawnCommand(teammates, displayMode = 'auto') {
231
- // Agent Teams are spawned via Claude Code's Task tool with subagent_type="general-purpose"
232
- // and special teammate configs. This is a conceptual representation.
233
-
234
- const command = {
235
- tool: 'Task',
236
- subagent_type: 'general-purpose',
237
- agent_teams_enabled: true,
238
- display_mode: displayMode,
239
- teammates: teammates.map(t => ({
240
- id: t.id,
241
- role: t.role,
242
- icon: t.icon,
243
- prompt: t.spawnPrompt
244
- }))
245
- };
246
-
247
- return JSON.stringify(command, null, 2);
307
+ export function generateSpawnCommand(teammates, _displayMode = 'auto') {
308
+ // Returns legacy JSON format for any code still calling the old signature.
309
+ // New code should call generateTeamInstructions(teamHierarchy, featureName, phase).
310
+ return JSON.stringify({
311
+ _deprecated: true,
312
+ _message: 'Use generateTeamInstructions() for Agent Teams mode.',
313
+ teammates: teammates.map(t => ({ id: t.id, role: t.role })),
314
+ }, null, 2);
248
315
  }
249
316
 
250
317
  /**
251
- * Main orchestration function - decides if/when to spawn Agent Teams
318
+ * Main orchestration function decides if/when to use Agent Teams vs subagents.
319
+ *
252
320
  * @param {Object} options
253
321
  * @param {string} options.projectPath - Project root path
254
322
  * @param {string} options.featureName - Feature name
@@ -267,22 +335,22 @@ export async function orchestrateTeam(options) {
267
335
  activeAgents = [],
268
336
  complexity,
269
337
  estimatedFiles = 0,
270
- multiDomain = false
338
+ multiDomain = false,
271
339
  } = options;
272
340
 
273
- // Step 1: Decide if Agent Teams should spawn
274
- const spawnDecision = shouldSpawnAgentTeam({
341
+ // Step 1: Decide mode
342
+ const modeDecision = shouldSpawnAgentTeam({
275
343
  activeAgents,
276
344
  complexity,
277
345
  estimatedFiles,
278
- multiDomain
346
+ multiDomain,
279
347
  });
280
348
 
281
- if (!spawnDecision.shouldSpawn) {
349
+ if (modeDecision.mode === 'single') {
282
350
  return {
283
- useAgentTeams: false,
284
- reason: spawnDecision.reason,
285
- recommendation: 'Use single session with Task tool for subagents'
351
+ mode: 'single',
352
+ reason: modeDecision.reason,
353
+ recommendation: 'Use single session sequential implementation',
286
354
  };
287
355
  }
288
356
 
@@ -290,25 +358,39 @@ export async function orchestrateTeam(options) {
290
358
  const agentsConfig = await loadAgentsConfig(projectPath);
291
359
  const teamHierarchy = buildTeamHierarchy(activeAgents, agentsConfig);
292
360
 
293
- // Step 3: Generate teammate configs
294
- const teammates = generateTeammateConfigs(teamHierarchy, featureName, phase);
361
+ if (modeDecision.mode === 'agent-teams') {
362
+ // Step 3a: Generate natural language Agent Teams instructions
363
+ const teamInstructions = generateTeamInstructions(teamHierarchy, featureName, phase);
295
364
 
296
- // Step 4: Generate spawn command
297
- const spawnCommand = generateSpawnCommand(teammates, spawnDecision.recommendedMode);
365
+ return {
366
+ mode: 'agent-teams',
367
+ reason: modeDecision.reason,
368
+ teamHierarchy: {
369
+ teamLead: teamHierarchy.teamLead?.id,
370
+ squads: Object.keys(teamHierarchy.squads),
371
+ totalTeammates: teamHierarchy.totalTeammates,
372
+ validators: teamHierarchy.validators.map(v => v.id),
373
+ },
374
+ teamInstructions,
375
+ recommendation: `Create Agent Team with ${teamHierarchy.totalTeammates} teammates (lead + domain leaders). Require plan approval before implementation.`,
376
+ };
377
+ }
378
+
379
+ // mode === 'subagents'
380
+ // Step 3b: Generate Task tool subagent configs
381
+ const teammates = generateTeammateConfigs(teamHierarchy, featureName, phase);
298
382
 
299
383
  return {
300
- useAgentTeams: true,
301
- reason: spawnDecision.reason,
302
- displayMode: spawnDecision.recommendedMode,
384
+ mode: 'subagents',
385
+ reason: modeDecision.reason,
303
386
  teamHierarchy: {
304
387
  teamLead: teamHierarchy.teamLead?.id,
305
388
  squads: Object.keys(teamHierarchy.squads),
306
389
  totalTeammates: teamHierarchy.totalTeammates,
307
- validators: teamHierarchy.validators.map(v => v.id)
390
+ validators: teamHierarchy.validators.map(v => v.id),
308
391
  },
309
392
  teammates,
310
- spawnCommand,
311
- recommendation: `Spawn ${teamHierarchy.totalTeammates} teammates in ${spawnDecision.recommendedMode} mode`
393
+ recommendation: `Dispatch ${teammates.length} Task subagents in parallel`,
312
394
  };
313
395
  }
314
396
 
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Phase Eligibility Checker
3
+ *
4
+ * Determines whether a feature is eligible to auto-advance to the next phase.
5
+ * Used by phase-runner.js and the dispatch.js PostToolUse hook.
6
+ *
7
+ * Eligibility criteria (all must pass):
8
+ * 1. Required outputs for the current phase exist on disk
9
+ * 2. No tasks in state with status = 'blocked'
10
+ * 3. validationHistory passRate >= minPassRate (from workflow config)
11
+ * 4. Trust level meets the workflow's trustLevel requirement
12
+ */
13
+
14
+ import { existsSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { loadState } from '../../core/state/state-manager.js';
17
+ import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
18
+ import { derivePhase } from '../../core/state/state-manager.js';
19
+ import { PHASES } from '../../commands/state/validate-phase.js';
20
+ import { shouldAutoApprove } from '../trust/trust-manager.js';
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // Types (JSDoc)
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * @typedef {Object} EligibilityBlocker
28
+ * @property {'missing_outputs'|'blocked_tasks'|'low_pass_rate'|'trust_too_low'|'state_error'} type
29
+ * @property {string[]} [items] - Specific items causing the block
30
+ * @property {number} [current] - Current value (for rate/level comparisons)
31
+ * @property {number|string} [required] - Required value
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} EligibilityResult
36
+ * @property {boolean} eligible
37
+ * @property {EligibilityBlocker[]} blockers
38
+ * @property {string} currentPhase
39
+ * @property {string|null} nextPhase
40
+ * @property {number|null} passRate - Current validation pass rate (null if no data)
41
+ */
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Helpers
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /** Phase order (canonical from advance-phase.js) */
48
+ const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks', 'implement', 'sync'];
49
+
50
+ /**
51
+ * Get the next phase in the standard sequence.
52
+ * Returns null when at the last phase.
53
+ */
54
+ function getNextPhase(currentPhase) {
55
+ const idx = PHASE_ORDER.indexOf(currentPhase);
56
+ if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null;
57
+ return PHASE_ORDER[idx + 1];
58
+ }
59
+
60
+ /**
61
+ * Compute the overall validation pass rate from validationHistory.
62
+ * Returns null when no history exists.
63
+ *
64
+ * @param {Object} validationHistory - feature.validationHistory
65
+ * @returns {number|null}
66
+ */
67
+ export function computePassRate(validationHistory) {
68
+ if (!validationHistory || Object.keys(validationHistory).length === 0) return null;
69
+
70
+ const entries = Object.values(validationHistory);
71
+ const total = entries.length;
72
+ const passed = entries.filter(e => e.status === 'passed').length;
73
+
74
+ return total > 0 ? passed / total : null;
75
+ }
76
+
77
+ /**
78
+ * Check if required outputs exist on disk for the given phase.
79
+ *
80
+ * @param {string} featureName
81
+ * @param {string} phase
82
+ * @param {string} projectPath
83
+ * @returns {string[]} Array of missing output descriptions
84
+ */
85
+ function getMissingRequiredOutputs(featureName, phase, projectPath) {
86
+ const phaseDef = PHASES[phase];
87
+ if (!phaseDef?.requiredOutputs) return [];
88
+
89
+ const missing = [];
90
+ const featureBase = join(projectPath, '.morph', 'features', featureName);
91
+
92
+ // Map output type names to common file paths
93
+ const outputPathMap = {
94
+ 'proposal': '0-proposal/proposal.md',
95
+ 'spec': '1-design/spec.md',
96
+ 'contracts': '1-design/contracts.cs',
97
+ 'tasks': '3-tasks/tasks.md',
98
+ 'schemaAnalysis': '1-design/schema-analysis.md',
99
+ };
100
+
101
+ for (const output of phaseDef.requiredOutputs) {
102
+ const relPath = outputPathMap[output];
103
+ if (relPath && !existsSync(join(featureBase, relPath))) {
104
+ missing.push(output);
105
+ }
106
+ }
107
+
108
+ return missing;
109
+ }
110
+
111
+ /**
112
+ * Get tasks that are blocked (attempt >= 3, no resolution).
113
+ *
114
+ * @param {Object} feature - Feature state object
115
+ * @returns {string[]} Array of blocked task IDs
116
+ */
117
+ function getBlockedTasks(feature) {
118
+ const history = feature.validationHistory || {};
119
+ return Object.entries(history)
120
+ .filter(([, entry]) => entry.status === 'blocked')
121
+ .map(([taskId]) => taskId);
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // Main export
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Check whether a feature is eligible to auto-advance to the next phase.
130
+ *
131
+ * @param {string} featureName
132
+ * @param {Object} [opts]
133
+ * @param {string} [opts.projectPath] - Project root (defaults to cwd)
134
+ * @param {boolean} [opts.dryRun] - Only compute, don't enforce
135
+ * @returns {EligibilityResult}
136
+ */
137
+ export function checkPhaseEligibility(featureName, opts = {}) {
138
+ const projectPath = opts.projectPath || process.cwd();
139
+
140
+ // ── Load state ────────────────────────────────────────────────────────────
141
+ const state = loadState(false);
142
+ if (!state) {
143
+ return {
144
+ eligible: false,
145
+ blockers: [{ type: 'state_error', items: ['state.json not found'] }],
146
+ currentPhase: 'unknown',
147
+ nextPhase: null,
148
+ passRate: null,
149
+ };
150
+ }
151
+
152
+ const feature = state.features?.[featureName];
153
+ if (!feature) {
154
+ return {
155
+ eligible: false,
156
+ blockers: [{ type: 'state_error', items: [`Feature '${featureName}' not found in state.json`] }],
157
+ currentPhase: 'unknown',
158
+ nextPhase: null,
159
+ passRate: null,
160
+ };
161
+ }
162
+
163
+ // ── Derive current phase ──────────────────────────────────────────────────
164
+ const featureFolderPath = join(projectPath, '.morph', 'features', featureName);
165
+ const currentPhase = feature.phase || derivePhase(featureFolderPath);
166
+ const nextPhase = getNextPhase(currentPhase);
167
+
168
+ if (!nextPhase) {
169
+ return {
170
+ eligible: false,
171
+ blockers: [{ type: 'state_error', items: ['Feature is at the final phase'] }],
172
+ currentPhase,
173
+ nextPhase: null,
174
+ passRate: null,
175
+ };
176
+ }
177
+
178
+ // ── Load workflow config ──────────────────────────────────────────────────
179
+ let workflowConfig = null;
180
+ if (feature.workflow && feature.workflow !== 'auto') {
181
+ try {
182
+ workflowConfig = getWorkflowConfig(feature.workflow);
183
+ } catch {
184
+ // Non-blocking: fall through to defaults
185
+ }
186
+ }
187
+
188
+ const minPassRate = workflowConfig?.phaseChain?.pauseOn
189
+ ? 0.80 // Default minimum
190
+ : workflowConfig?.eligibility?.minPassRate ?? 0.80;
191
+
192
+ const blockers = [];
193
+
194
+ // ── Check 1: Missing required outputs ────────────────────────────────────
195
+ const missingOutputs = getMissingRequiredOutputs(featureName, currentPhase, projectPath);
196
+ if (missingOutputs.length > 0) {
197
+ blockers.push({ type: 'missing_outputs', items: missingOutputs });
198
+ }
199
+
200
+ // ── Check 2: Blocked tasks ────────────────────────────────────────────────
201
+ const blockedTasks = getBlockedTasks(feature);
202
+ if (blockedTasks.length > 0) {
203
+ blockers.push({ type: 'blocked_tasks', items: blockedTasks });
204
+ }
205
+
206
+ // ── Check 3: Validation pass rate ────────────────────────────────────────
207
+ const passRate = computePassRate(feature.validationHistory);
208
+ if (passRate !== null && passRate < minPassRate) {
209
+ blockers.push({
210
+ type: 'low_pass_rate',
211
+ current: passRate,
212
+ required: minPassRate,
213
+ });
214
+ }
215
+
216
+ // ── Check 4: Trust level ─────────────────────────────────────────────────
217
+ try {
218
+ const requiredGateMap = { design: 'design', tasks: 'tasks', uiux: 'uiux' };
219
+ const gate = requiredGateMap[currentPhase];
220
+ if (gate) {
221
+ const trustResult = shouldAutoApprove(featureName, gate);
222
+ if (!trustResult.autoApprove) {
223
+ const gateStatus = feature.approvalGates?.[gate];
224
+ if (!gateStatus?.approved) {
225
+ blockers.push({
226
+ type: 'trust_too_low',
227
+ items: [`Gate '${gate}' requires trust level ${trustResult.level || 'medium+'}`],
228
+ });
229
+ }
230
+ }
231
+ }
232
+ } catch {
233
+ // Trust manager unavailable — non-blocking, don't add blocker
234
+ }
235
+
236
+ return {
237
+ eligible: blockers.length === 0,
238
+ blockers,
239
+ currentPhase,
240
+ nextPhase,
241
+ passRate,
242
+ };
243
+ }