@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.
- package/CLAUDE.md +3 -0
- package/LICENSE +71 -71
- package/README.md +379 -379
- package/bin/morph-spec.js +89 -0
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +206 -206
- package/framework/agents/README.md +14 -14
- package/framework/agents/architecture/standards-architect.md +159 -159
- package/framework/agents/frontend/blazor-builder.md +13 -0
- package/framework/agents/frontend/css-specialist.md +87 -0
- package/framework/agents/frontend/nextjs-expert.md +13 -0
- package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
- package/framework/agents/frontend/ui-ux-designer.md +80 -10
- package/framework/agents/infrastructure/azure-architect.md +149 -149
- package/framework/agents/infrastructure/infra-architect.md +48 -48
- package/framework/agents.json +2268 -2567
- package/framework/hooks/README.md +204 -204
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
- package/framework/hooks/shared/phase-utils.js +139 -139
- package/framework/hooks/shared/worktree-helpers.js +30 -8
- package/framework/rules/csharp-standards.md +13 -13
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +16 -16
- package/framework/rules/testing-standards.md +14 -14
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
- package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2040 -2040
- package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
- package/framework/templates/README.md +814 -814
- package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
- package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
- package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
- package/framework/templates/code/dotnet/contracts/README.md +74 -74
- package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
- package/framework/templates/code/dotnet/frontend/component.razor +239 -239
- package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
- package/framework/templates/code/dotnet/jobs/job.cs +171 -171
- package/framework/templates/code/dotnet/test.cs +239 -239
- package/framework/templates/code/sql/rls-policy.sql +57 -57
- package/framework/templates/code/sql/supabase-migration.sql +100 -100
- package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
- package/framework/templates/code/typescript/contracts.ts +168 -168
- package/framework/templates/context/CONTEXT.md +181 -181
- package/framework/templates/context/design-system.md +211 -0
- package/framework/templates/docs/proposal.md +182 -182
- package/framework/templates/docs/user-stories.md +34 -34
- package/framework/templates/examples/design-system-examples.md +357 -357
- package/framework/templates/examples/spec-examples.md +90 -90
- package/framework/templates/feature/decisions.md +187 -187
- package/framework/templates/feature/recap.md +146 -146
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/infrastructure/azure/README.md +286 -286
- package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
- package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
- package/package.json +82 -82
- package/src/commands/agents/agent-metrics.js +174 -0
- package/src/commands/agents/audit-dispatch.js +82 -0
- package/src/commands/agents/detect-agents.js +275 -0
- package/src/commands/agents/dispatch-agents.js +41 -135
- package/src/commands/agents/index.js +13 -4
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +59 -0
- package/src/commands/project/install-plugin-cmd.js +28 -28
- package/src/commands/project/setup-infra-cmd.js +12 -12
- package/src/commands/project/update.js +1 -17
- package/src/commands/project/worktree.js +58 -2
- package/src/commands/state/advance-phase.js +11 -3
- package/src/commands/tasks/index.js +4 -4
- package/src/commands/templates/index.js +5 -5
- package/src/commands/utils/index.js +3 -3
- package/src/commands/validation/index.js +4 -4
- package/src/core/state/phase-state-machine.js +217 -217
- package/src/core/state/state-manager.js +81 -0
- package/src/lib/agents/agents-manager.js +349 -0
- package/src/lib/agents/metrics-calculator.js +125 -0
- package/src/lib/agents/output-collector.js +75 -0
- package/src/lib/generators/context-generator.js +8 -7
- package/src/lib/generators/design-system-generator.js +298 -298
- package/src/lib/generators/index.js +10 -10
- package/src/lib/generators/metadata-extractor.js +387 -387
- package/src/lib/stack-filter.js +65 -65
- package/src/lib/standards/index.js +7 -7
- package/src/lib/standards/standards-context-injector.js +302 -302
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/architecture/architecture-validator.js +387 -387
- package/src/lib/validators/architecture/index.js +7 -7
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
- package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
- package/src/lib/validators/blazor/blazor-validator.js +369 -369
- package/src/lib/validators/blazor/index.js +9 -9
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +287 -279
- package/src/lib/validators/content/index.js +7 -7
- package/src/lib/validators/contracts/index.js +7 -7
- package/src/lib/validators/css/css-validator.js +352 -352
- package/src/lib/validators/css/index.js +7 -7
- package/src/lib/validators/design-system/design-system-validator.js +231 -231
- package/src/lib/validators/design-system/index.js +7 -7
- package/src/lib/validators/nextjs/index.js +6 -6
- package/src/lib/validators/nextjs/next-component-validator.js +183 -183
- package/src/lib/validators/packages/index.js +7 -7
- package/src/lib/validators/packages/package-validator.js +360 -360
- package/src/lib/validators/shared/index.js +12 -12
- package/src/lib/validators/shared/issue-counter.js +18 -18
- package/src/lib/validators/shared/result-formatter.js +124 -124
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/ui/index.js +7 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/global-install.js +58 -58
- package/src/scripts/install-plugin.js +126 -126
- package/src/scripts/setup-infra.js +1 -17
- package/src/utils/agent-id-mapper.js +11 -0
- package/src/utils/agents-installer.js +40 -8
- package/src/utils/claude-md-injector.js +90 -90
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +200 -196
- package/src/utils/hooks-installer.js +556 -739
- package/src/utils/logger.js +32 -32
- package/src/utils/skills-installer.js +178 -178
- package/src/utils/tier-helpers.js +24 -0
- package/src/utils/version-checker.js +175 -175
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/templates/.idea/morph-templates.xml +0 -92
- 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
|
+
}
|