@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
@@ -1,217 +1,217 @@
1
- /**
2
- * Phase State Machine - Enforces valid phase transitions
3
- *
4
- * Prevents invalid workflow jumps and ensures sequential phase progression.
5
- * Based on the MORPH 5-phase workflow.
6
- */
7
-
8
- /**
9
- * Valid phase transitions map
10
- * Each phase can only transition to specific next phases
11
- */
12
- const VALID_TRANSITIONS = {
13
- 'proposal': ['setup'],
14
- 'setup': ['uiux', 'design'], // Can skip UI/UX if no frontend
15
- 'uiux': ['design'],
16
- 'design': ['clarify'],
17
- 'clarify': ['plan'],
18
- 'plan': ['tasks'],
19
- 'tasks': ['implement'],
20
- 'implement': ['sync', 'archived'], // Can skip sync
21
- 'sync': ['archived']
22
- };
23
-
24
- /**
25
- * Phase display names for error messages
26
- */
27
- const PHASE_NAMES = {
28
- 'proposal': 'Proposal (Phase 0)',
29
- 'setup': 'Setup (Phase 1)',
30
- 'uiux': 'UI/UX Design (Phase 1.5)',
31
- 'design': 'Design (Phase 2)',
32
- 'clarify': 'Clarify (Phase 3)',
33
- 'plan': 'Plan (Phase 4)',
34
- 'tasks': 'Tasks (Phase 5)',
35
- 'implement': 'Implement (Phase 6)',
36
- 'sync': 'Sync (Phase 7)',
37
- 'archived': 'Archived'
38
- };
39
-
40
- /**
41
- * Optional phases that can be skipped
42
- */
43
- const OPTIONAL_PHASES = ['uiux', 'sync'];
44
-
45
- /**
46
- * Validate if a phase transition is allowed
47
- * @param {string} fromPhase - Current phase
48
- * @param {string} toPhase - Target phase
49
- * @returns {boolean} True if transition is valid
50
- */
51
- export function isValidTransition(fromPhase, toPhase) {
52
- if (!fromPhase || !toPhase) {
53
- return false;
54
- }
55
-
56
- const validNextPhases = VALID_TRANSITIONS[fromPhase];
57
- return validNextPhases ? validNextPhases.includes(toPhase) : false;
58
- }
59
-
60
- /**
61
- * Get list of valid next phases for a given phase
62
- * @param {string} currentPhase - Current phase
63
- * @returns {string[]} Array of valid next phase names
64
- */
65
- export function getValidNextPhases(currentPhase) {
66
- return VALID_TRANSITIONS[currentPhase] || [];
67
- }
68
-
69
- /**
70
- * Get display name for a phase
71
- * @param {string} phase - Phase identifier
72
- * @returns {string} Human-readable phase name
73
- */
74
- export function getPhaseDisplayName(phase) {
75
- return PHASE_NAMES[phase] || phase;
76
- }
77
-
78
- /**
79
- * Check if a phase is optional
80
- * @param {string} phase - Phase identifier
81
- * @returns {boolean} True if phase is optional
82
- */
83
- export function isOptionalPhase(phase) {
84
- return OPTIONAL_PHASES.includes(phase);
85
- }
86
-
87
- /**
88
- * Validate transition and throw error if invalid
89
- * @param {string} fromPhase - Current phase
90
- * @param {string} toPhase - Target phase
91
- * @throws {Error} If transition is invalid
92
- */
93
- export function validateTransition(fromPhase, toPhase) {
94
- if (!fromPhase) {
95
- throw new Error('Current phase is undefined. Cannot validate transition.');
96
- }
97
-
98
- if (!toPhase) {
99
- throw new Error('Target phase is undefined. Cannot validate transition.');
100
- }
101
-
102
- if (!isValidTransition(fromPhase, toPhase)) {
103
- const validPhases = getValidNextPhases(fromPhase);
104
- const fromDisplay = getPhaseDisplayName(fromPhase);
105
- const toDisplay = getPhaseDisplayName(toPhase);
106
-
107
- throw new Error(
108
- `Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
109
- `Valid next phases from ${fromDisplay}:\n` +
110
- validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
111
- '\n\n' +
112
- 'You cannot skip phases in the MORPH workflow.\n' +
113
- 'To override this check, use the --force flag (not recommended).'
114
- );
115
- }
116
- }
117
-
118
- /**
119
- * Get the sequential order of phases
120
- * @returns {string[]} Ordered list of phases
121
- */
122
- export function getPhaseSequence() {
123
- return [
124
- 'proposal',
125
- 'setup',
126
- 'uiux', // optional
127
- 'design',
128
- 'clarify',
129
- 'plan',
130
- 'tasks',
131
- 'implement',
132
- 'sync', // optional
133
- 'archived'
134
- ];
135
- }
136
-
137
- /**
138
- * Get phase order index (for comparison)
139
- * @param {string} phase - Phase identifier
140
- * @returns {number} Index in sequence (-1 if not found)
141
- */
142
- export function getPhaseOrder(phase) {
143
- return getPhaseSequence().indexOf(phase);
144
- }
145
-
146
- /**
147
- * Check if target phase is ahead of current phase
148
- * @param {string} currentPhase - Current phase
149
- * @param {string} targetPhase - Target phase
150
- * @returns {boolean} True if target is ahead
151
- */
152
- export function isPhaseAhead(currentPhase, targetPhase) {
153
- return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
154
- }
155
-
156
- /**
157
- * Check if target phase is behind current phase (rollback scenario)
158
- * @param {string} currentPhase - Current phase
159
- * @param {string} targetPhase - Target phase
160
- * @returns {boolean} True if target is behind
161
- */
162
- export function isPhaseRollback(currentPhase, targetPhase) {
163
- return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
164
- }
165
-
166
- /**
167
- * Get all skipped phases between two phases
168
- * @param {string} fromPhase - Starting phase
169
- * @param {string} toPhase - Ending phase
170
- * @returns {string[]} Array of skipped phases
171
- */
172
- export function getSkippedPhases(fromPhase, toPhase) {
173
- const sequence = getPhaseSequence();
174
- const fromIndex = sequence.indexOf(fromPhase);
175
- const toIndex = sequence.indexOf(toPhase);
176
-
177
- if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
178
- return [];
179
- }
180
-
181
- return sequence.slice(fromIndex + 1, toIndex);
182
- }
183
-
184
- /**
185
- * Validate that no required phases were skipped
186
- * @param {string} fromPhase - Starting phase
187
- * @param {string} toPhase - Ending phase
188
- * @throws {Error} If required phases were skipped
189
- */
190
- export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
191
- const skipped = getSkippedPhases(fromPhase, toPhase);
192
- const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
193
-
194
- if (requiredSkipped.length > 0) {
195
- throw new Error(
196
- `Cannot skip required phases:\n` +
197
- requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
198
- '\n\nYou must complete each required phase in sequence.'
199
- );
200
- }
201
- }
202
-
203
- /**
204
- * Get comprehensive phase information
205
- * @param {string} phase - Phase identifier
206
- * @returns {Object} Phase metadata
207
- */
208
- export function getPhaseInfo(phase) {
209
- return {
210
- id: phase,
211
- displayName: getPhaseDisplayName(phase),
212
- order: getPhaseOrder(phase),
213
- isOptional: isOptionalPhase(phase),
214
- validNextPhases: getValidNextPhases(phase),
215
- canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
216
- };
217
- }
1
+ /**
2
+ * Phase State Machine - Enforces valid phase transitions
3
+ *
4
+ * Prevents invalid workflow jumps and ensures sequential phase progression.
5
+ * Based on the MORPH 5-phase workflow.
6
+ */
7
+
8
+ /**
9
+ * Valid phase transitions map
10
+ * Each phase can only transition to specific next phases
11
+ */
12
+ const VALID_TRANSITIONS = {
13
+ 'proposal': ['setup'],
14
+ 'setup': ['uiux', 'design'], // Can skip UI/UX if no frontend
15
+ 'uiux': ['design'],
16
+ 'design': ['clarify'],
17
+ 'clarify': ['plan'],
18
+ 'plan': ['tasks'],
19
+ 'tasks': ['implement'],
20
+ 'implement': ['sync', 'archived'], // Can skip sync
21
+ 'sync': ['archived']
22
+ };
23
+
24
+ /**
25
+ * Phase display names for error messages
26
+ */
27
+ const PHASE_NAMES = {
28
+ 'proposal': 'Proposal (Phase 0)',
29
+ 'setup': 'Setup (Phase 1)',
30
+ 'uiux': 'UI/UX Design (Phase 1.5)',
31
+ 'design': 'Design (Phase 2)',
32
+ 'clarify': 'Clarify (Phase 3)',
33
+ 'plan': 'Plan (Phase 4)',
34
+ 'tasks': 'Tasks (Phase 5)',
35
+ 'implement': 'Implement (Phase 6)',
36
+ 'sync': 'Sync (Phase 7)',
37
+ 'archived': 'Archived'
38
+ };
39
+
40
+ /**
41
+ * Optional phases that can be skipped
42
+ */
43
+ const OPTIONAL_PHASES = ['uiux', 'sync'];
44
+
45
+ /**
46
+ * Validate if a phase transition is allowed
47
+ * @param {string} fromPhase - Current phase
48
+ * @param {string} toPhase - Target phase
49
+ * @returns {boolean} True if transition is valid
50
+ */
51
+ export function isValidTransition(fromPhase, toPhase) {
52
+ if (!fromPhase || !toPhase) {
53
+ return false;
54
+ }
55
+
56
+ const validNextPhases = VALID_TRANSITIONS[fromPhase];
57
+ return validNextPhases ? validNextPhases.includes(toPhase) : false;
58
+ }
59
+
60
+ /**
61
+ * Get list of valid next phases for a given phase
62
+ * @param {string} currentPhase - Current phase
63
+ * @returns {string[]} Array of valid next phase names
64
+ */
65
+ export function getValidNextPhases(currentPhase) {
66
+ return VALID_TRANSITIONS[currentPhase] || [];
67
+ }
68
+
69
+ /**
70
+ * Get display name for a phase
71
+ * @param {string} phase - Phase identifier
72
+ * @returns {string} Human-readable phase name
73
+ */
74
+ export function getPhaseDisplayName(phase) {
75
+ return PHASE_NAMES[phase] || phase;
76
+ }
77
+
78
+ /**
79
+ * Check if a phase is optional
80
+ * @param {string} phase - Phase identifier
81
+ * @returns {boolean} True if phase is optional
82
+ */
83
+ export function isOptionalPhase(phase) {
84
+ return OPTIONAL_PHASES.includes(phase);
85
+ }
86
+
87
+ /**
88
+ * Validate transition and throw error if invalid
89
+ * @param {string} fromPhase - Current phase
90
+ * @param {string} toPhase - Target phase
91
+ * @throws {Error} If transition is invalid
92
+ */
93
+ export function validateTransition(fromPhase, toPhase) {
94
+ if (!fromPhase) {
95
+ throw new Error('Current phase is undefined. Cannot validate transition.');
96
+ }
97
+
98
+ if (!toPhase) {
99
+ throw new Error('Target phase is undefined. Cannot validate transition.');
100
+ }
101
+
102
+ if (!isValidTransition(fromPhase, toPhase)) {
103
+ const validPhases = getValidNextPhases(fromPhase);
104
+ const fromDisplay = getPhaseDisplayName(fromPhase);
105
+ const toDisplay = getPhaseDisplayName(toPhase);
106
+
107
+ throw new Error(
108
+ `Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
109
+ `Valid next phases from ${fromDisplay}:\n` +
110
+ validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
111
+ '\n\n' +
112
+ 'You cannot skip phases in the MORPH workflow.\n' +
113
+ 'To override this check, use the --force flag (not recommended).'
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get the sequential order of phases
120
+ * @returns {string[]} Ordered list of phases
121
+ */
122
+ export function getPhaseSequence() {
123
+ return [
124
+ 'proposal',
125
+ 'setup',
126
+ 'uiux', // optional
127
+ 'design',
128
+ 'clarify',
129
+ 'plan',
130
+ 'tasks',
131
+ 'implement',
132
+ 'sync', // optional
133
+ 'archived'
134
+ ];
135
+ }
136
+
137
+ /**
138
+ * Get phase order index (for comparison)
139
+ * @param {string} phase - Phase identifier
140
+ * @returns {number} Index in sequence (-1 if not found)
141
+ */
142
+ export function getPhaseOrder(phase) {
143
+ return getPhaseSequence().indexOf(phase);
144
+ }
145
+
146
+ /**
147
+ * Check if target phase is ahead of current phase
148
+ * @param {string} currentPhase - Current phase
149
+ * @param {string} targetPhase - Target phase
150
+ * @returns {boolean} True if target is ahead
151
+ */
152
+ export function isPhaseAhead(currentPhase, targetPhase) {
153
+ return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
154
+ }
155
+
156
+ /**
157
+ * Check if target phase is behind current phase (rollback scenario)
158
+ * @param {string} currentPhase - Current phase
159
+ * @param {string} targetPhase - Target phase
160
+ * @returns {boolean} True if target is behind
161
+ */
162
+ export function isPhaseRollback(currentPhase, targetPhase) {
163
+ return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
164
+ }
165
+
166
+ /**
167
+ * Get all skipped phases between two phases
168
+ * @param {string} fromPhase - Starting phase
169
+ * @param {string} toPhase - Ending phase
170
+ * @returns {string[]} Array of skipped phases
171
+ */
172
+ export function getSkippedPhases(fromPhase, toPhase) {
173
+ const sequence = getPhaseSequence();
174
+ const fromIndex = sequence.indexOf(fromPhase);
175
+ const toIndex = sequence.indexOf(toPhase);
176
+
177
+ if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
178
+ return [];
179
+ }
180
+
181
+ return sequence.slice(fromIndex + 1, toIndex);
182
+ }
183
+
184
+ /**
185
+ * Validate that no required phases were skipped
186
+ * @param {string} fromPhase - Starting phase
187
+ * @param {string} toPhase - Ending phase
188
+ * @throws {Error} If required phases were skipped
189
+ */
190
+ export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
191
+ const skipped = getSkippedPhases(fromPhase, toPhase);
192
+ const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
193
+
194
+ if (requiredSkipped.length > 0) {
195
+ throw new Error(
196
+ `Cannot skip required phases:\n` +
197
+ requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
198
+ '\n\nYou must complete each required phase in sequence.'
199
+ );
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get comprehensive phase information
205
+ * @param {string} phase - Phase identifier
206
+ * @returns {Object} Phase metadata
207
+ */
208
+ export function getPhaseInfo(phase) {
209
+ return {
210
+ id: phase,
211
+ displayName: getPhaseDisplayName(phase),
212
+ order: getPhaseOrder(phase),
213
+ isOptional: isOptionalPhase(phase),
214
+ validNextPhases: getValidNextPhases(phase),
215
+ canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
216
+ };
217
+ }
@@ -279,6 +279,7 @@ async function ensureFeature(featureName, options = {}) {
279
279
  pending: 0
280
280
  },
281
281
  checkpoints: [],
282
+ dispatchLog: [],
282
283
  trustConfig: {},
283
284
  contextBundles: [],
284
285
  fileChanges: []
@@ -420,6 +421,29 @@ export function removeAgent(featureName, agentId) {
420
421
  return false;
421
422
  }
422
423
 
424
+ /**
425
+ * Log a dispatch event to state.json.
426
+ * Appends entry to feature.dispatchLog, capped at 50 entries (oldest trimmed).
427
+ *
428
+ * @param {string} featureName - Feature name
429
+ * @param {{ timestamp: string, phase: string, mode: string, agents: Array<{ id: string, subagentType: string, group: string, tasks: string[] }> }} entry - Dispatch event to log
430
+ */
431
+ export function logDispatch(featureName, entry) {
432
+ const state = loadState();
433
+ const feature = state.features?.[featureName];
434
+ if (!feature) return;
435
+
436
+ if (!Array.isArray(feature.dispatchLog)) feature.dispatchLog = [];
437
+ feature.dispatchLog.push(entry);
438
+
439
+ // Cap at 50 entries
440
+ if (feature.dispatchLog.length > 50) {
441
+ feature.dispatchLog = feature.dispatchLog.slice(-50);
442
+ }
443
+
444
+ saveState(state);
445
+ }
446
+
423
447
  /**
424
448
  * Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
425
449
  * Supports both kebab-case and camelCase input formats for all output types
@@ -807,3 +831,60 @@ export function getApprovalHistory(featureName) {
807
831
  // Sort by timestamp
808
832
  return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
809
833
  }
834
+
835
+ /**
836
+ * Find which agent was assigned a specific task in the latest dispatch.
837
+ * Searches only the last dispatchLog entry.
838
+ *
839
+ * @param {string} featureName
840
+ * @param {string} taskId
841
+ * @returns {string|null} Agent ID or null if not found
842
+ */
843
+ export function resolveAgentForTask(featureName, taskId) {
844
+ const state = loadState();
845
+ const feature = state.features?.[featureName];
846
+ if (!feature) return null;
847
+
848
+ const log = feature.dispatchLog;
849
+ if (!Array.isArray(log) || log.length === 0) return null;
850
+
851
+ const latest = log[log.length - 1];
852
+ for (const agent of latest.agents || []) {
853
+ if (Array.isArray(agent.tasks) && agent.tasks.includes(taskId)) {
854
+ return agent.id;
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+
860
+ /**
861
+ * Update timing fields on a specific agent in the latest dispatch entry.
862
+ * Fail-open: returns silently if feature/dispatch/agent not found.
863
+ *
864
+ * @param {string} featureName
865
+ * @param {string} agentId - Agent ID to update
866
+ * @param {Object} updates - Fields to merge (startedAt, completedAt, tasksCompleted)
867
+ */
868
+ export function updateDispatchAgentTiming(featureName, agentId, updates) {
869
+ const state = loadState();
870
+ const feature = state.features?.[featureName];
871
+ if (!feature) return;
872
+
873
+ const log = feature.dispatchLog;
874
+ if (!Array.isArray(log) || log.length === 0) return;
875
+
876
+ const latest = log[log.length - 1];
877
+ const agent = (latest.agents || []).find(a => a.id === agentId);
878
+ if (!agent) return;
879
+
880
+ Object.assign(agent, updates);
881
+
882
+ // If all agents now have completedAt, set entry.completedAt to the latest
883
+ const allComplete = (latest.agents || []).every(a => a.completedAt);
884
+ if (allComplete && latest.agents.length > 0) {
885
+ const timestamps = latest.agents.map(a => new Date(a.completedAt).getTime());
886
+ latest.completedAt = new Date(Math.max(...timestamps)).toISOString();
887
+ }
888
+
889
+ saveState(state);
890
+ }