@polymorphism-tech/morph-spec 7.9.3 → 8.0.0

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 (213) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +31 -2
  3. package/bin/morph-spec.js +50 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +38 -1
  7. package/docs/QUICKSTART.md +60 -1
  8. package/framework/CLAUDE.md +13 -2
  9. package/framework/MORPH.md +38 -2
  10. package/framework/agents.json +22 -3
  11. package/framework/commands/commit.md +166 -166
  12. package/framework/commands/morph-hotfix.md +39 -0
  13. package/framework/commands/morph-infra.md +215 -215
  14. package/framework/commands/morph-preflight.md +205 -205
  15. package/framework/commands/morph-troubleshoot.md +124 -124
  16. package/framework/hooks/README.md +25 -10
  17. package/framework/hooks/claude-code/core/gate-guard.js +4 -2
  18. package/framework/hooks/claude-code/core/loop-detect.js +2 -1
  19. package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
  20. package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
  21. package/framework/hooks/claude-code/core/validate-completion.js +1 -1
  22. package/framework/hooks/claude-code/core/worktree-context.js +160 -0
  23. package/framework/hooks/claude-code/statusline.py +679 -679
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/activity-logger.js +160 -160
  31. package/framework/hooks/shared/build-check.js +2 -2
  32. package/framework/hooks/shared/hook-response.js +17 -5
  33. package/framework/hooks/shared/payload-utils.js +42 -0
  34. package/framework/hooks/shared/state-reader.js +22 -2
  35. package/framework/hooks/shared/stdin-reader.js +26 -26
  36. package/framework/rubrics/backend.json +41 -41
  37. package/framework/rules/ai-agents-standards.md +45 -45
  38. package/framework/rules/dotnet-templates.md +86 -86
  39. package/framework/rules/frontend-standards.md +13 -13
  40. package/framework/rules/infrastructure-standards.md +27 -27
  41. package/framework/rules/neon-standards.md +25 -25
  42. package/framework/rules/nextjs-standards.md +20 -20
  43. package/framework/rules/nextjs-templates.md +40 -40
  44. package/framework/schemas/feature.schema.json +95 -1
  45. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  46. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  47. package/framework/skills/core/morph-apply/SKILL.md +39 -8
  48. package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
  49. package/framework/skills/core/morph-eval/SKILL.md +6 -1
  50. package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
  51. package/framework/skills/core/morph-implement/SKILL.md +8 -4
  52. package/framework/skills/core/morph-plan/SKILL.md +5 -1
  53. package/framework/skills/core/morph-proposal/SKILL.md +24 -1
  54. package/framework/skills/core/morph-review/SKILL.md +3 -2
  55. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  56. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  57. package/framework/standards/STANDARDS.json +7 -2
  58. package/framework/standards/ai-agents/agent-session.md +160 -160
  59. package/framework/standards/ai-agents/batching.md +193 -193
  60. package/framework/standards/ai-agents/context-providers.md +188 -188
  61. package/framework/standards/ai-agents/mcp-server.md +250 -250
  62. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  63. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  64. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  65. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  66. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  67. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  68. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  69. package/framework/standards/ai-agents/production.md +275 -275
  70. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  71. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  72. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  73. package/framework/standards/ai-agents/structured-output.md +249 -249
  74. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  75. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +45 -3
  76. package/framework/templates/.idea/morph-templates.xml +92 -0
  77. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  78. package/framework/templates/IDE-SNIPPETS.md +262 -262
  79. package/framework/templates/REGISTRY.json +0 -62
  80. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  81. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  82. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  83. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  84. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  85. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  86. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  87. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  88. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  89. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  91. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  92. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  93. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  94. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  95. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  96. package/framework/templates/code/typescript/contracts.ts +167 -167
  97. package/framework/templates/docs/clarifications.md +253 -253
  98. package/framework/templates/docs/schema-analysis.md +119 -119
  99. package/framework/templates/docs/ui-components.md +52 -52
  100. package/framework/templates/docs/ui-design-system.md +76 -76
  101. package/framework/templates/docs/ui-flows.md +167 -167
  102. package/framework/templates/docs/ui-mockups.md +98 -98
  103. package/framework/templates/feature/decisions.md +122 -122
  104. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  107. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  108. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  109. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  110. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  111. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  112. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  113. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  114. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  115. package/framework/templates/infrastructure/github/README.md +56 -56
  116. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  117. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  118. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  119. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  120. package/framework/templates/integrations/asaas-client.cs +387 -387
  121. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  122. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  123. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  124. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  125. package/framework/templates/project/validate.js +120 -120
  126. package/framework/templates/state.template.json +17 -17
  127. package/framework/templates/ui/design-system.css +226 -226
  128. package/package.json +1 -1
  129. package/src/commands/dashboard/dashboard.js +53 -53
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/mcp/mcp-setup.js +519 -519
  132. package/src/commands/project/checkpoint.js +209 -209
  133. package/src/commands/project/doctor.js +4 -2
  134. package/src/commands/project/init.js +46 -2
  135. package/src/commands/project/update.js +5 -3
  136. package/src/commands/project/worktree.js +1005 -46
  137. package/src/commands/state/advance.js +17 -2
  138. package/src/commands/state/approve.js +18 -1
  139. package/src/commands/state/create.js +158 -14
  140. package/src/commands/state/retype.js +126 -0
  141. package/src/commands/state/score.js +4 -0
  142. package/src/commands/tasks/dag.js +32 -0
  143. package/src/commands/validation/index.js +1 -0
  144. package/src/commands/validation/validate-feature.js +33 -5
  145. package/src/commands/validation/verify.js +190 -0
  146. package/src/core/state/phase-state-machine.js +197 -197
  147. package/src/core/state/state-manager.js +35 -10
  148. package/src/core/templates/template-renderer.js +477 -477
  149. package/src/core/work-types.js +79 -0
  150. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  151. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  152. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  153. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  154. package/src/dashboard/app/main.jsx +13 -13
  155. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  156. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  157. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  158. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  159. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  160. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  161. package/src/dashboard/app/pages/Standards.jsx +332 -332
  162. package/src/dashboard/app/styles/index.css +47 -47
  163. package/src/dashboard/index.html +12 -12
  164. package/src/dashboard/package.json +24 -24
  165. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  166. package/src/dashboard/scanner/phases-scanner.js +15 -15
  167. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  168. package/src/dashboard/scanner/rules-scanner.js +37 -37
  169. package/src/dashboard/scanner/skills-scanner.js +43 -43
  170. package/src/dashboard/scanner/squads-scanner.js +13 -13
  171. package/src/dashboard/scanner/standards-scanner.js +26 -26
  172. package/src/dashboard/scanner/state-scanner.js +42 -0
  173. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  174. package/src/dashboard/vite.config.js +24 -24
  175. package/src/lib/agents/metrics-aggregator.js +56 -56
  176. package/src/lib/agents/metrics-calculator.js +125 -125
  177. package/src/lib/agents/output-collector.js +73 -73
  178. package/src/lib/detectors/claude-config-detector.js +152 -152
  179. package/src/lib/detectors/design-system-detector.js +231 -231
  180. package/src/lib/detectors/index.js +217 -217
  181. package/src/lib/generators/architecture-scanner.js +639 -639
  182. package/src/lib/generators/design-system-generator.js +508 -508
  183. package/src/lib/generators/index.js +13 -13
  184. package/src/lib/generators/settings-generator.js +209 -209
  185. package/src/lib/hooks/hook-executor.js +167 -167
  186. package/src/lib/installers/mcp-installer.js +719 -719
  187. package/src/lib/installers/memory-installer.js +1 -1
  188. package/src/lib/risk-detector.js +16 -0
  189. package/src/lib/tasks/dispatch-planner.js +122 -4
  190. package/src/lib/tasks/mermaid-renderer.js +132 -0
  191. package/src/lib/tasks/task-classifier.js +30 -30
  192. package/src/lib/tasks/task-parser.js +209 -209
  193. package/src/lib/tasks/test-runner.js +27 -6
  194. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  195. package/src/lib/validators/checkpoint-persistence.js +39 -39
  196. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  197. package/src/lib/validators/css/css-validator.js +340 -340
  198. package/src/lib/validators/packages/package-validator.js +347 -347
  199. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  200. package/src/lib/validators/shared/issue-counter.js +14 -14
  201. package/src/lib/validators/validation-runner.js +349 -349
  202. package/src/lib/validators/verify-runner.js +322 -0
  203. package/src/lib/work-type-detector.js +143 -0
  204. package/src/scripts/setup-infra.js +32 -5
  205. package/src/utils/agent-id-mapper.js +11 -11
  206. package/src/utils/agents-installer.js +10 -6
  207. package/src/utils/claude-settings-manager.js +300 -300
  208. package/src/utils/file-copier.js +43 -0
  209. package/src/utils/hooks-installer.js +41 -11
  210. package/src/utils/skills-installer.js +174 -174
  211. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  212. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  213. package/src/lib/tasks/wave-resolver.js +0 -353
@@ -0,0 +1,190 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
6
+ import { runVerify } from '../../lib/validators/verify-runner.js';
7
+ import { resolvePrimaryRoot } from '../project/worktree.js';
8
+
9
+ /** ✓ pass · ✗ fail · – skip */
10
+ function statusIcon(status) {
11
+ if (status === 'pass') return chalk.green('✓');
12
+ if (status === 'fail') return chalk.red('✗');
13
+ return chalk.gray('–');
14
+ }
15
+
16
+ /**
17
+ * Human-readable rendering of a runVerify() result — mirrors the
18
+ * ✓/✗-per-node + feedback-list style of formatScoreBreakdown() in
19
+ * score-aggregator.js, adapted to verify's build/tests/validators nodes.
20
+ */
21
+ function formatVerifyResult(result) {
22
+ const lines = [];
23
+ const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
24
+
25
+ lines.push('');
26
+ lines.push(chalk.bold(`Verify — ${scopeLabel}`));
27
+ lines.push(chalk.gray(`rubric: ${result.rubric}${result.stack ? ` | stack: ${result.stack}` : ''}`));
28
+ lines.push('');
29
+ lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
30
+ lines.push('');
31
+ lines.push('Nodes:');
32
+ for (const [name, node] of Object.entries(result.nodes)) {
33
+ const icon = statusIcon(node.status);
34
+ let detail;
35
+ if (node.status === 'skip') {
36
+ detail = node.reason || '';
37
+ } else if (name === 'validators' && node.score != null) {
38
+ detail = `score ${node.score}/10`;
39
+ } else {
40
+ detail = `${node.durationMs}ms`;
41
+ }
42
+ lines.push(` ${icon} ${name.padEnd(11)} ${node.status.padEnd(5)} ${chalk.gray(detail)}`);
43
+ }
44
+
45
+ if (result.feedback.length > 0) {
46
+ lines.push('');
47
+ lines.push('Feedback:');
48
+ for (const line of result.feedback) lines.push(` ${line}`);
49
+ }
50
+
51
+ lines.push('');
52
+ return lines.join('\n');
53
+ }
54
+
55
+ /**
56
+ * Decide whether it's safe to persist a verify result to feature.json.
57
+ *
58
+ * Safe contexts: the primary git root, or the feature's own REGISTERED
59
+ * worktree (`worktree.path` — where feature.json is authoritative and
60
+ * travels with the branch). Any other worktree (e.g. a harness subagent's
61
+ * temporary scratch worktree) must never write feature.json — that would
62
+ * risk a merge conflict / violate the single-writer clause.
63
+ *
64
+ * Worktree registration is ROOT-OWNED data: `runWorktreeSetup` (see
65
+ * src/commands/project/worktree.js) writes `.worktree` ONLY into the PRIMARY
66
+ * ROOT's copy of feature.json (via saveState after chdir'ing back to root) —
67
+ * it never touches the worktree's own branch-local copy. So this must read
68
+ * the ROOT's feature.json to find the registration, never the cwd-local one
69
+ * (`getFeature(featureName)` resolves relative to `process.cwd()`, which
70
+ * inside a worktree is the branch-local copy and will never carry
71
+ * `.worktree` — reading it here would make this branch permanently dead code
72
+ * for the exact case it exists to handle: verify running from inside a
73
+ * registered worktree).
74
+ *
75
+ * @param {{ cwd: string, featureName: string, noPersist: boolean }} params
76
+ * @returns {{ persist: boolean, reason: string|null }}
77
+ */
78
+ function decidePersistence({ cwd, featureName, noPersist }) {
79
+ if (noPersist) return { persist: false, reason: 'no-persist-flag' };
80
+
81
+ const root = resolvePrimaryRoot(cwd);
82
+ const normalizedCwd = resolve(cwd).replace(/\\/g, '/');
83
+
84
+ if (normalizedCwd === root) return { persist: true, reason: null };
85
+
86
+ // cwd is (or looks like) a worktree — consult the ROOT's feature.json for
87
+ // the registration. Fail-open: an unreadable/missing root feature.json
88
+ // means we can't confirm registration, so treat as unregistered.
89
+ let registeredWt = null;
90
+ try {
91
+ const rootFeatureJsonPath = join(root, '.morph', 'features', featureName, 'feature.json');
92
+ const rootFeature = JSON.parse(readFileSync(rootFeatureJsonPath, 'utf8'));
93
+ registeredWt = rootFeature.worktree?.path
94
+ ? String(rootFeature.worktree.path).replace(/\\/g, '/')
95
+ : null;
96
+ } catch {
97
+ registeredWt = null;
98
+ }
99
+
100
+ if (registeredWt && normalizedCwd === registeredWt) return { persist: true, reason: null };
101
+ return { persist: false, reason: 'unregistered-worktree' };
102
+ }
103
+
104
+ /** Read tasks.json's task list for a feature; [] if missing/corrupt. */
105
+ function readTasks(featureName) {
106
+ try {
107
+ const tasksPath = getAbsoluteOutputPath(process.cwd(), featureName, 'tasks');
108
+ if (!existsSync(tasksPath)) return [];
109
+ return JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * `morph-spec verify <feature> [task]` — the deterministic code-before-judge
117
+ * gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
118
+ * for the node cadence; this command wraps it with CLI ergonomics
119
+ * (human/JSON output, exit codes) and context-aware persistence.
120
+ *
121
+ * Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node) ·
122
+ * 2 = usage error (unknown feature or task — the task's `group` decides the
123
+ * rubric, so an unknown task id is a hard usage error here, unlike the
124
+ * advisory warning `morph-spec score` gives for the same situation).
125
+ */
126
+ export async function verifyCommand(featureName, taskId, options = {}) {
127
+ let feature;
128
+ try {
129
+ feature = getFeature(featureName);
130
+ } catch {
131
+ feature = null;
132
+ }
133
+ if (!feature) {
134
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
135
+ process.exit(2);
136
+ }
137
+
138
+ if (taskId) {
139
+ const tasks = readTasks(featureName);
140
+ if (!tasks.some((t) => t.id === taskId)) {
141
+ console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
142
+ process.exit(2);
143
+ }
144
+ }
145
+
146
+ const result = await runVerify(process.cwd(), featureName, taskId || null, {
147
+ rubric: options.rubric,
148
+ timeout: options.timeout,
149
+ skipTests: options.skipTests,
150
+ skipBuild: options.skipBuild,
151
+ });
152
+
153
+ const scopeKey = taskId ? `taskVerifications.${taskId}` : 'verifications.feature';
154
+ const { persist, reason } = decidePersistence({
155
+ cwd: process.cwd(),
156
+ featureName,
157
+ noPersist: options.noPersist,
158
+ });
159
+
160
+ let compact = null;
161
+ if (persist) {
162
+ const prevEntry = taskId ? feature.taskVerifications?.[taskId] : feature.verifications?.feature;
163
+ compact = {
164
+ status: result.status,
165
+ nodes: {
166
+ build: result.nodes.build.status,
167
+ tests: result.nodes.tests.status,
168
+ validators: result.nodes.validators.status,
169
+ },
170
+ score: result.nodes.validators.score ?? null,
171
+ attempts: (prevEntry?.attempts ?? 0) + 1,
172
+ updatedAt: new Date().toISOString(),
173
+ };
174
+ await updateFeature(featureName, scopeKey, compact);
175
+ }
176
+
177
+ if (options.json) {
178
+ const output = { ...result, persisted: persist, ...(persist ? {} : { reason }) };
179
+ console.log(JSON.stringify(output, null, 2));
180
+ } else {
181
+ console.log(formatVerifyResult(result));
182
+ if (persist) {
183
+ console.log(chalk.gray(`Persisted: ${scopeKey} (attempts: ${compact.attempts})\n`));
184
+ } else {
185
+ console.log(chalk.yellow(`⚠️ Not persisted (${reason})\n`));
186
+ }
187
+ }
188
+
189
+ process.exit(result.status === 'pass' ? 0 : 1);
190
+ }
@@ -1,197 +1,197 @@
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': ['uiux', 'plan'],
14
- 'uiux': ['plan'],
15
- 'plan': ['implement'],
16
- 'implement': ['review'],
17
- 'review': []
18
- };
19
-
20
- /**
21
- * Phase display names for error messages
22
- */
23
- const PHASE_NAMES = {
24
- 'proposal': 'Proposal (Phase 0)',
25
- 'uiux': 'UI/UX Design (Phase 1)',
26
- 'plan': 'Plan (Phase 2)',
27
- 'implement': 'Implement (Phase 3)',
28
- 'review': 'Quality Review (Phase 4)'
29
- };
30
-
31
- /**
32
- * Optional phases that can be skipped
33
- */
34
- const OPTIONAL_PHASES = ['uiux'];
35
-
36
- /**
37
- * Validate if a phase transition is allowed
38
- * @param {string} fromPhase - Current phase
39
- * @param {string} toPhase - Target phase
40
- * @returns {boolean} True if transition is valid
41
- */
42
- export function isValidTransition(fromPhase, toPhase) {
43
- if (!fromPhase || !toPhase) {
44
- return false;
45
- }
46
-
47
- const validNextPhases = VALID_TRANSITIONS[fromPhase];
48
- return validNextPhases ? validNextPhases.includes(toPhase) : false;
49
- }
50
-
51
- /**
52
- * Get list of valid next phases for a given phase
53
- * @param {string} currentPhase - Current phase
54
- * @returns {string[]} Array of valid next phase names
55
- */
56
- export function getValidNextPhases(currentPhase) {
57
- return VALID_TRANSITIONS[currentPhase] || [];
58
- }
59
-
60
- /**
61
- * Get display name for a phase
62
- * @param {string} phase - Phase identifier
63
- * @returns {string} Human-readable phase name
64
- */
65
- export function getPhaseDisplayName(phase) {
66
- return PHASE_NAMES[phase] || phase;
67
- }
68
-
69
- /**
70
- * Check if a phase is optional
71
- * @param {string} phase - Phase identifier
72
- * @returns {boolean} True if phase is optional
73
- */
74
- export function isOptionalPhase(phase) {
75
- return OPTIONAL_PHASES.includes(phase);
76
- }
77
-
78
- /**
79
- * Validate transition and throw error if invalid
80
- * @param {string} fromPhase - Current phase
81
- * @param {string} toPhase - Target phase
82
- * @throws {Error} If transition is invalid
83
- */
84
- export function validateTransition(fromPhase, toPhase) {
85
- if (!fromPhase) {
86
- throw new Error('Current phase is undefined. Cannot validate transition.');
87
- }
88
-
89
- if (!toPhase) {
90
- throw new Error('Target phase is undefined. Cannot validate transition.');
91
- }
92
-
93
- if (!isValidTransition(fromPhase, toPhase)) {
94
- const validPhases = getValidNextPhases(fromPhase);
95
- const fromDisplay = getPhaseDisplayName(fromPhase);
96
- const toDisplay = getPhaseDisplayName(toPhase);
97
-
98
- throw new Error(
99
- `Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
100
- `Valid next phases from ${fromDisplay}:\n` +
101
- validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
102
- '\n\n' +
103
- 'You cannot skip phases in the MORPH workflow.\n' +
104
- 'To override this check, use the --force flag (not recommended).'
105
- );
106
- }
107
- }
108
-
109
- /**
110
- * Get the sequential order of phases
111
- * @returns {string[]} Ordered list of phases
112
- */
113
- export function getPhaseSequence() {
114
- return ['proposal', 'uiux', 'plan', 'implement', 'review'];
115
- }
116
-
117
- /**
118
- * Get phase order index (for comparison)
119
- * @param {string} phase - Phase identifier
120
- * @returns {number} Index in sequence (-1 if not found)
121
- */
122
- export function getPhaseOrder(phase) {
123
- return getPhaseSequence().indexOf(phase);
124
- }
125
-
126
- /**
127
- * Check if target phase is ahead of current phase
128
- * @param {string} currentPhase - Current phase
129
- * @param {string} targetPhase - Target phase
130
- * @returns {boolean} True if target is ahead
131
- */
132
- export function isPhaseAhead(currentPhase, targetPhase) {
133
- return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
134
- }
135
-
136
- /**
137
- * Check if target phase is behind current phase (rollback scenario)
138
- * @param {string} currentPhase - Current phase
139
- * @param {string} targetPhase - Target phase
140
- * @returns {boolean} True if target is behind
141
- */
142
- export function isPhaseRollback(currentPhase, targetPhase) {
143
- return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
144
- }
145
-
146
- /**
147
- * Get all skipped phases between two phases
148
- * @param {string} fromPhase - Starting phase
149
- * @param {string} toPhase - Ending phase
150
- * @returns {string[]} Array of skipped phases
151
- */
152
- export function getSkippedPhases(fromPhase, toPhase) {
153
- const sequence = getPhaseSequence();
154
- const fromIndex = sequence.indexOf(fromPhase);
155
- const toIndex = sequence.indexOf(toPhase);
156
-
157
- if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
158
- return [];
159
- }
160
-
161
- return sequence.slice(fromIndex + 1, toIndex);
162
- }
163
-
164
- /**
165
- * Validate that no required phases were skipped
166
- * @param {string} fromPhase - Starting phase
167
- * @param {string} toPhase - Ending phase
168
- * @throws {Error} If required phases were skipped
169
- */
170
- export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
171
- const skipped = getSkippedPhases(fromPhase, toPhase);
172
- const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
173
-
174
- if (requiredSkipped.length > 0) {
175
- throw new Error(
176
- `Cannot skip required phases:\n` +
177
- requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
178
- '\n\nYou must complete each required phase in sequence.'
179
- );
180
- }
181
- }
182
-
183
- /**
184
- * Get comprehensive phase information
185
- * @param {string} phase - Phase identifier
186
- * @returns {Object} Phase metadata
187
- */
188
- export function getPhaseInfo(phase) {
189
- return {
190
- id: phase,
191
- displayName: getPhaseDisplayName(phase),
192
- order: getPhaseOrder(phase),
193
- isOptional: isOptionalPhase(phase),
194
- validNextPhases: getValidNextPhases(phase),
195
- canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
196
- };
197
- }
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': ['uiux', 'plan'],
14
+ 'uiux': ['plan'],
15
+ 'plan': ['implement'],
16
+ 'implement': ['review'],
17
+ 'review': []
18
+ };
19
+
20
+ /**
21
+ * Phase display names for error messages
22
+ */
23
+ const PHASE_NAMES = {
24
+ 'proposal': 'Proposal (Phase 0)',
25
+ 'uiux': 'UI/UX Design (Phase 1)',
26
+ 'plan': 'Plan (Phase 2)',
27
+ 'implement': 'Implement (Phase 3)',
28
+ 'review': 'Quality Review (Phase 4)'
29
+ };
30
+
31
+ /**
32
+ * Optional phases that can be skipped
33
+ */
34
+ const OPTIONAL_PHASES = ['uiux'];
35
+
36
+ /**
37
+ * Validate if a phase transition is allowed
38
+ * @param {string} fromPhase - Current phase
39
+ * @param {string} toPhase - Target phase
40
+ * @returns {boolean} True if transition is valid
41
+ */
42
+ export function isValidTransition(fromPhase, toPhase) {
43
+ if (!fromPhase || !toPhase) {
44
+ return false;
45
+ }
46
+
47
+ const validNextPhases = VALID_TRANSITIONS[fromPhase];
48
+ return validNextPhases ? validNextPhases.includes(toPhase) : false;
49
+ }
50
+
51
+ /**
52
+ * Get list of valid next phases for a given phase
53
+ * @param {string} currentPhase - Current phase
54
+ * @returns {string[]} Array of valid next phase names
55
+ */
56
+ export function getValidNextPhases(currentPhase) {
57
+ return VALID_TRANSITIONS[currentPhase] || [];
58
+ }
59
+
60
+ /**
61
+ * Get display name for a phase
62
+ * @param {string} phase - Phase identifier
63
+ * @returns {string} Human-readable phase name
64
+ */
65
+ export function getPhaseDisplayName(phase) {
66
+ return PHASE_NAMES[phase] || phase;
67
+ }
68
+
69
+ /**
70
+ * Check if a phase is optional
71
+ * @param {string} phase - Phase identifier
72
+ * @returns {boolean} True if phase is optional
73
+ */
74
+ export function isOptionalPhase(phase) {
75
+ return OPTIONAL_PHASES.includes(phase);
76
+ }
77
+
78
+ /**
79
+ * Validate transition and throw error if invalid
80
+ * @param {string} fromPhase - Current phase
81
+ * @param {string} toPhase - Target phase
82
+ * @throws {Error} If transition is invalid
83
+ */
84
+ export function validateTransition(fromPhase, toPhase) {
85
+ if (!fromPhase) {
86
+ throw new Error('Current phase is undefined. Cannot validate transition.');
87
+ }
88
+
89
+ if (!toPhase) {
90
+ throw new Error('Target phase is undefined. Cannot validate transition.');
91
+ }
92
+
93
+ if (!isValidTransition(fromPhase, toPhase)) {
94
+ const validPhases = getValidNextPhases(fromPhase);
95
+ const fromDisplay = getPhaseDisplayName(fromPhase);
96
+ const toDisplay = getPhaseDisplayName(toPhase);
97
+
98
+ throw new Error(
99
+ `Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
100
+ `Valid next phases from ${fromDisplay}:\n` +
101
+ validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
102
+ '\n\n' +
103
+ 'You cannot skip phases in the MORPH workflow.\n' +
104
+ 'To override this check, use the --force flag (not recommended).'
105
+ );
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get the sequential order of phases
111
+ * @returns {string[]} Ordered list of phases
112
+ */
113
+ export function getPhaseSequence() {
114
+ return ['proposal', 'uiux', 'plan', 'implement', 'review'];
115
+ }
116
+
117
+ /**
118
+ * Get phase order index (for comparison)
119
+ * @param {string} phase - Phase identifier
120
+ * @returns {number} Index in sequence (-1 if not found)
121
+ */
122
+ export function getPhaseOrder(phase) {
123
+ return getPhaseSequence().indexOf(phase);
124
+ }
125
+
126
+ /**
127
+ * Check if target phase is ahead of current phase
128
+ * @param {string} currentPhase - Current phase
129
+ * @param {string} targetPhase - Target phase
130
+ * @returns {boolean} True if target is ahead
131
+ */
132
+ export function isPhaseAhead(currentPhase, targetPhase) {
133
+ return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
134
+ }
135
+
136
+ /**
137
+ * Check if target phase is behind current phase (rollback scenario)
138
+ * @param {string} currentPhase - Current phase
139
+ * @param {string} targetPhase - Target phase
140
+ * @returns {boolean} True if target is behind
141
+ */
142
+ export function isPhaseRollback(currentPhase, targetPhase) {
143
+ return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
144
+ }
145
+
146
+ /**
147
+ * Get all skipped phases between two phases
148
+ * @param {string} fromPhase - Starting phase
149
+ * @param {string} toPhase - Ending phase
150
+ * @returns {string[]} Array of skipped phases
151
+ */
152
+ export function getSkippedPhases(fromPhase, toPhase) {
153
+ const sequence = getPhaseSequence();
154
+ const fromIndex = sequence.indexOf(fromPhase);
155
+ const toIndex = sequence.indexOf(toPhase);
156
+
157
+ if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
158
+ return [];
159
+ }
160
+
161
+ return sequence.slice(fromIndex + 1, toIndex);
162
+ }
163
+
164
+ /**
165
+ * Validate that no required phases were skipped
166
+ * @param {string} fromPhase - Starting phase
167
+ * @param {string} toPhase - Ending phase
168
+ * @throws {Error} If required phases were skipped
169
+ */
170
+ export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
171
+ const skipped = getSkippedPhases(fromPhase, toPhase);
172
+ const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
173
+
174
+ if (requiredSkipped.length > 0) {
175
+ throw new Error(
176
+ `Cannot skip required phases:\n` +
177
+ requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
178
+ '\n\nYou must complete each required phase in sequence.'
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get comprehensive phase information
185
+ * @param {string} phase - Phase identifier
186
+ * @returns {Object} Phase metadata
187
+ */
188
+ export function getPhaseInfo(phase) {
189
+ return {
190
+ id: phase,
191
+ displayName: getPhaseDisplayName(phase),
192
+ order: getPhaseOrder(phase),
193
+ isOptional: isOptionalPhase(phase),
194
+ validNextPhases: getValidNextPhases(phase),
195
+ canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
196
+ };
197
+ }
@@ -87,7 +87,7 @@ function readProjectFromConfig() {
87
87
  * Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
88
88
  * for the brief Windows lock race. Creates the parent directory if needed.
89
89
  */
90
- function atomicWriteJson(targetPath, data) {
90
+ export function atomicWriteJson(targetPath, data) {
91
91
  const dir = dirname(targetPath);
92
92
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
93
93
 
@@ -118,8 +118,14 @@ function atomicWriteJson(targetPath, data) {
118
118
  * mutation on feature A would otherwise rewrite sibling B's file from A's loaded
119
119
  * copy. Skipping unchanged siblings means A never touches B's authoritative file
120
120
  * in the common case — shrinking the cross-feature clobber window to nothing
121
- * unless B genuinely changed under A. A formatting mismatch only costs a
122
- * redundant (idempotent) write, never correctness.
121
+ * unless B genuinely changed under A.
122
+ *
123
+ * A formatting mismatch is NOT free, though: the guard compares bytes, so a
124
+ * sibling committed with a trailing newline fails it and gets rewritten. The
125
+ * content is equivalent, but git sees ` M` on a file this caller never meant to
126
+ * touch — enough to fail a whole-tree clean check (see worktreeFinishCommand).
127
+ * And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
128
+ * write to a sibling would still be clobbered by the sweep.
123
129
  */
124
130
  function writeFeatureJson(featureName, feature) {
125
131
  const path = getFeatureJsonPath(featureName);
@@ -327,8 +333,17 @@ export function saveState(state) {
327
333
  state.metadata.completedFeatures = Object.values(features)
328
334
  .filter((f) => f && f.status === 'completed').length;
329
335
 
330
- // 1) Persist each feature to its authoritative, committed feature.json. This
331
- // is what travels with the feature's own commit — no cross-feature sweep.
336
+ // 1) Persist each feature to its authoritative, committed feature.json — what
337
+ // travels with the feature's own commit.
338
+ //
339
+ // NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
340
+ // every sibling; writeFeatureJson's byte-identical guard makes that a no-op
341
+ // only while a sibling's committed file matches our formatting exactly. A
342
+ // sibling saved with a trailing newline (any editor, prettier) is rewritten
343
+ // and shows up as ` M` — dirtying the tree on behalf of a feature the
344
+ // caller never touched. Callers that need to change ONE feature's field
345
+ // should write that feature.json directly instead of round-tripping state
346
+ // through here (see worktreeFinishCommand's registration cleanup).
332
347
  const thinFeatures = {};
333
348
  for (const [name, feature] of Object.entries(features)) {
334
349
  writeFeatureJson(name, feature);
@@ -487,19 +502,24 @@ async function ensureFeature(featureName) {
487
502
  /**
488
503
  * Explicitly create a feature with the canonical shape (documented in
489
504
  * framework/schemas/feature.schema.json). Idempotent: an existing feature is
490
- * returned untouched. This is the server-side birth path behind
491
- * `morph-spec create` — replacing the implicit registration on the first
492
- * Write into .morph/features/ (the state-sync hook remains as fallback).
505
+ * returned untouched (including its `workType` — a later `--type` on a
506
+ * repeat call never overwrites it; see `morph-spec retype` for that). This
507
+ * is the server-side birth path behind `morph-spec create` — replacing the
508
+ * implicit registration on the first Write into .morph/features/ (the
509
+ * state-sync hook remains as fallback).
493
510
  *
494
511
  * Works even before any state exists (scaffolds the root index), so a
495
512
  * feature can be born as the very first morph action in a project.
496
513
  *
497
514
  * @param {string} featureName - Feature name (kebab-case)
498
- * @param {{ status?: string }} [options]
515
+ * @param {{ status?: string, workType?: string, workTypeDetection?: object }} [options]
516
+ * `workType` defaults to 'feature' (v8 routing — see src/core/work-types.js).
517
+ * `workTypeDetection` is only written when provided (source/confidence/matched
518
+ * audit trail from the `--type`/`--request` resolution in `create.js`).
499
519
  * @returns {Promise<{ feature: Object, created: boolean }>}
500
520
  */
501
521
  export async function createFeature(featureName, options = {}) {
502
- const { status = 'in_progress' } = options;
522
+ const { status = 'in_progress', workType, workTypeDetection } = options;
503
523
  const state = loadState(false) || scaffoldState();
504
524
 
505
525
  const existing = state.features[featureName];
@@ -510,6 +530,7 @@ export async function createFeature(featureName, options = {}) {
510
530
  const now = new Date().toISOString();
511
531
  state.features[featureName] = {
512
532
  status,
533
+ workType: workType || 'feature',
513
534
  createdAt: now,
514
535
  updatedAt: now,
515
536
  activeAgents: [],
@@ -534,6 +555,10 @@ export async function createFeature(featureName, options = {}) {
534
555
  fileChanges: []
535
556
  };
536
557
 
558
+ if (workTypeDetection) {
559
+ state.features[featureName].workTypeDetection = workTypeDetection;
560
+ }
561
+
537
562
  state.metadata.totalFeatures = Object.keys(state.features).length;
538
563
  saveState(state);
539
564
  return { feature: state.features[featureName], created: true };