@polymorphism-tech/morph-spec 4.3.6 → 4.5.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 (375) hide show
  1. package/.morph/.morphversion +3 -3
  2. package/.morph/analytics/threads-log.jsonl +44 -9
  3. package/.morph/config/config.json +2 -3
  4. package/.morph/framework/standards/STANDARDS.json +812 -0
  5. package/.morph/{standards → framework/standards}/ai-agents/team-orchestration.md +3 -3
  6. package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
  7. package/.morph/{templates → framework/templates}/README.md +17 -17
  8. package/.morph/{templates → framework/templates}/REGISTRY.json +48 -233
  9. package/.morph/framework/templates/code/dotnet/contracts/contracts.cs.hbs +172 -0
  10. package/.morph/{templates → framework/templates}/context/CONTEXT-FEATURE.md +1 -1
  11. package/.morph/{templates → framework/templates}/context/CONTEXT.md +3 -3
  12. package/.morph/framework/templates/docs/clarifications.md +253 -0
  13. package/.morph/framework/templates/docs/onboarding.md +123 -0
  14. package/.morph/framework/templates/docs/schema-analysis.md +119 -0
  15. package/.morph/{templates → framework/templates}/docs/spec.md +149 -149
  16. package/.morph/framework/templates/docs/ui-components.md +124 -0
  17. package/.morph/framework/templates/docs/ui-design-system.md +76 -0
  18. package/.morph/framework/templates/docs/ui-flows.md +167 -0
  19. package/.morph/framework/templates/docs/ui-mockups.md +98 -0
  20. package/.morph/{templates → framework/templates}/examples/spec-examples.md +1 -1
  21. package/.morph/{templates → framework/templates}/infrastructure/github/README.md +11 -11
  22. package/.morph/{templates → framework/templates}/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +2 -2
  23. package/.morph/{templates → framework/templates}/meta-prompts/parallel-workers/parallel-worker.md +2 -2
  24. package/.morph/{templates → framework/templates}/meta-prompts/validators/pre-commit-validator.md +1 -1
  25. package/.morph/logs/tool-failures.log +51 -0
  26. package/.morph/memory/pre-compact-2026-02-22T17-01-01-658Z.json +16 -0
  27. package/.morph/state.json +1 -1
  28. package/CLAUDE.md +20 -119
  29. package/README.md +20 -18
  30. package/bin/detect-agents.js +1 -1
  31. package/bin/morph-spec.js +116 -266
  32. package/bin/task-manager.cjs +2 -2
  33. package/bin/validate.js +1 -1
  34. package/claude-plugin.json +14 -0
  35. package/docs/claude-alignment-report.md +137 -0
  36. package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +512 -0
  37. package/docs/plans/2026-02-22-claude-settings.md +515 -0
  38. package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +728 -0
  39. package/docs/plans/2026-02-22-morph-spec-next.md +478 -0
  40. package/docs/plans/2026-02-22-native-alignment-design.md +199 -0
  41. package/docs/plans/2026-02-22-native-alignment-impl.md +925 -0
  42. package/docs/plans/2026-02-22-native-enrichment-design.md +244 -0
  43. package/docs/plans/2026-02-22-native-enrichment.md +735 -0
  44. package/framework/CLAUDE.md +77 -0
  45. package/framework/commands/morph-apply.md +9 -9
  46. package/framework/commands/morph-archive.md +8 -8
  47. package/framework/commands/morph-infra.md +1 -1
  48. package/framework/commands/morph-proposal.md +9 -9
  49. package/framework/commands/morph-status.md +3 -3
  50. package/framework/commands/morph-troubleshoot.md +1 -1
  51. package/framework/hooks/README.md +201 -282
  52. package/framework/hooks/claude-code/notification/approval-reminder.js +52 -0
  53. package/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
  54. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
  55. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
  56. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
  57. package/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
  58. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
  59. package/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
  60. package/framework/hooks/claude-code/statusline.py +239 -0
  61. package/framework/hooks/claude-code/statusline.sh +7 -0
  62. package/framework/hooks/claude-code/stop/validate-completion.js +88 -0
  63. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
  64. package/framework/hooks/shared/hook-response.js +45 -0
  65. package/framework/hooks/shared/phase-utils.js +129 -0
  66. package/framework/hooks/shared/state-reader.js +138 -0
  67. package/framework/hooks/shared/stdin-reader.js +26 -0
  68. package/framework/phases.json +145 -0
  69. package/framework/rules/csharp-standards.md +10 -0
  70. package/framework/rules/frontend-standards.md +14 -0
  71. package/framework/rules/infrastructure-standards.md +13 -0
  72. package/framework/rules/morph-workflow.md +86 -0
  73. package/framework/rules/testing-standards.md +11 -0
  74. package/framework/skills/level-0-meta/brainstorming.md +133 -0
  75. package/framework/skills/level-0-meta/code-review.md +12 -4
  76. package/framework/skills/level-0-meta/mcp-registry.json +207 -0
  77. package/framework/skills/level-0-meta/morph-checklist.md +9 -1
  78. package/framework/skills/level-0-meta/simulation-checklist.md +9 -1
  79. package/framework/skills/level-0-meta/tool-usage-guide.md +335 -0
  80. package/framework/skills/level-0-meta/verification-before-completion.md +145 -0
  81. package/framework/skills/level-1-workflows/morph-replicate.md +9 -1
  82. package/framework/skills/level-1-workflows/phase-clarify.md +65 -4
  83. package/framework/skills/level-1-workflows/phase-codebase-analysis.md +182 -0
  84. package/framework/skills/level-1-workflows/phase-design.md +342 -80
  85. package/framework/skills/level-1-workflows/phase-implement.md +254 -0
  86. package/framework/skills/level-1-workflows/phase-setup.md +76 -10
  87. package/framework/skills/level-1-workflows/phase-tasks.md +88 -7
  88. package/framework/skills/level-1-workflows/phase-uiux.md +95 -17
  89. package/framework/skills/level-2-domains/ai-agents/ai-system-architect.md +8 -1
  90. package/framework/skills/level-2-domains/architecture/po-pm-advisor.md +8 -1
  91. package/framework/skills/level-2-domains/architecture/prompt-engineer.md +8 -1
  92. package/framework/skills/level-2-domains/architecture/seo-growth-hacker.md +8 -1
  93. package/framework/skills/level-2-domains/architecture/standards-architect.md +11 -4
  94. package/framework/skills/level-2-domains/backend/api-designer.md +8 -1
  95. package/framework/skills/level-2-domains/backend/dotnet-senior.md +8 -1
  96. package/framework/skills/level-2-domains/backend/ef-modeler.md +8 -1
  97. package/framework/skills/level-2-domains/backend/hangfire-orchestrator.md +9 -2
  98. package/framework/skills/level-2-domains/backend/ms-agent-expert.md +8 -1
  99. package/framework/skills/level-2-domains/frontend/blazor-builder.md +8 -1
  100. package/framework/skills/level-2-domains/frontend/nextjs-expert.md +8 -1
  101. package/framework/skills/level-2-domains/frontend/ui-ux-designer.md +9 -2
  102. package/framework/skills/level-2-domains/infrastructure/azure-architect.md +8 -1
  103. package/framework/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +8 -1
  104. package/framework/skills/level-2-domains/infrastructure/bicep-architect.md +8 -1
  105. package/framework/skills/level-2-domains/infrastructure/container-specialist.md +8 -1
  106. package/framework/skills/level-2-domains/infrastructure/devops-engineer.md +8 -1
  107. package/framework/skills/level-2-domains/integrations/asaas-financial.md +8 -1
  108. package/framework/skills/level-2-domains/integrations/azure-identity.md +8 -1
  109. package/framework/skills/level-2-domains/integrations/clerk-auth.md +8 -1
  110. package/framework/skills/level-2-domains/integrations/{hangfire-orchestrator.md → hangfire-integration.md} +8 -1
  111. package/framework/skills/level-2-domains/integrations/resend-email.md +8 -1
  112. package/framework/skills/level-2-domains/quality/code-analyzer.md +10 -3
  113. package/framework/skills/level-2-domains/quality/testing-specialist.md +8 -1
  114. package/framework/standards/STANDARDS.json +812 -0
  115. package/framework/standards/ai-agents/team-orchestration.md +3 -3
  116. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -0
  117. package/framework/standards/integration/mcp/mcp-tools.md +384 -0
  118. package/framework/templates/README.md +17 -17
  119. package/framework/templates/REGISTRY.json +48 -233
  120. package/framework/templates/code/dotnet/contracts/contracts.cs.hbs +172 -0
  121. package/framework/templates/context/CONTEXT-FEATURE.md +1 -1
  122. package/framework/templates/context/CONTEXT.md +3 -3
  123. package/framework/templates/docs/clarifications.md +253 -0
  124. package/framework/templates/docs/onboarding.md +123 -0
  125. package/framework/templates/docs/schema-analysis.md +119 -0
  126. package/framework/templates/docs/spec.md +149 -149
  127. package/framework/templates/docs/ui-components.md +124 -0
  128. package/framework/templates/docs/ui-design-system.md +76 -0
  129. package/framework/templates/docs/ui-flows.md +167 -0
  130. package/framework/templates/docs/ui-mockups.md +98 -0
  131. package/framework/templates/docs/user-stories.md +34 -0
  132. package/framework/templates/examples/spec-examples.md +1 -1
  133. package/framework/templates/infrastructure/github/README.md +11 -11
  134. package/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +2 -2
  135. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +2 -2
  136. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +1 -1
  137. package/framework/workflows/configs/express.json +45 -0
  138. package/framework/workflows/configs/spec-only.json +43 -0
  139. package/framework/workflows/docs/enforcement-pipeline.md +8 -8
  140. package/framework/workflows/docs/full-morph.md +3 -3
  141. package/package.json +3 -2
  142. package/scripts/generate-refs.js +336 -0
  143. package/scripts/generate-standards-registry.js +44 -0
  144. package/scripts/validate-real.mjs +255 -0
  145. package/src/commands/feature/create-story.js +362 -361
  146. package/src/commands/feature/shard-spec.js +225 -224
  147. package/src/commands/feature/sprint-status.js +1 -1
  148. package/src/commands/generation/generate-onboarding.js +169 -0
  149. package/src/commands/generation/generate.js +2 -2
  150. package/src/commands/mcp/mcp-setup.js +315 -0
  151. package/src/commands/project/changes.js +66 -0
  152. package/src/commands/project/checkpoint.js +209 -0
  153. package/src/commands/project/cost.js +179 -0
  154. package/src/commands/project/diff.js +278 -0
  155. package/src/commands/project/doctor.js +55 -7
  156. package/src/commands/project/init.js +318 -76
  157. package/src/commands/project/revert.js +173 -0
  158. package/src/commands/project/standards.js +80 -0
  159. package/src/commands/project/status.js +376 -0
  160. package/src/commands/project/update-agents.js +23 -0
  161. package/src/commands/project/update.js +63 -30
  162. package/src/commands/state/advance-phase.js +4 -3
  163. package/src/commands/state/state.js +10 -3
  164. package/src/commands/state/validate-phase.js +19 -2
  165. package/src/commands/templates/template-customize.js +4 -4
  166. package/src/commands/templates/template-render.js +1 -1
  167. package/src/commands/templates/template-show.js +1 -1
  168. package/src/commands/validation/validate-feature.js +359 -0
  169. package/src/core/orchestrator.js +3 -38
  170. package/src/core/paths/output-schema.js +135 -0
  171. package/src/core/state/state-manager.js +831 -592
  172. package/src/core/templates/template-registry.js +2 -2
  173. package/src/core/workflows/workflow-detector.js +17 -1
  174. package/src/lib/agents/micro-agent-factory.js +1 -1
  175. package/src/lib/context/context-bundler.js +2 -1
  176. package/src/lib/detectors/claude-config-detector.js +392 -0
  177. package/src/lib/detectors/conversation-analyzer.js +4 -4
  178. package/src/lib/detectors/design-system-detector.js +6 -5
  179. package/src/lib/detectors/standards-generator.js +2 -2
  180. package/src/lib/generators/context-generator.js +539 -538
  181. package/src/lib/generators/recap-generator.js +1 -1
  182. package/src/lib/generators/settings-generator.js +210 -0
  183. package/src/lib/hooks/hook-executor.js +1 -1
  184. package/src/lib/installers/mcp-installer.js +299 -0
  185. package/src/lib/learning/learning-system.js +3 -3
  186. package/src/lib/orchestration/team-orchestrator.js +1 -1
  187. package/src/lib/standards/standards-context-injector.js +7 -7
  188. package/src/lib/threads/thread-coordinator.js +1 -1
  189. package/src/lib/troubleshooting/troubleshoot-grep.js +1 -1
  190. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -273
  191. package/src/lib/validators/design-system/design-system-validator.js +1 -1
  192. package/src/lib/validators/spec-validator.js +258 -258
  193. package/src/lib/validators/validation-runner.js +270 -269
  194. package/src/utils/agents-installer.js +206 -0
  195. package/src/utils/claude-settings-manager.js +258 -0
  196. package/src/utils/file-copier.js +1 -1
  197. package/src/utils/hooks-installer.js +354 -28
  198. package/src/utils/skills-installer.js +74 -0
  199. package/.morph/project/context/detection-log.md +0 -16
  200. package/.morph/project/standards/inferred.md +0 -59
  201. package/framework/hooks/agent-stop/validate-and-continue.js +0 -96
  202. package/framework/hooks/agent-stop/validate-checkpoints.js +0 -101
  203. package/framework/hooks/agent-stop/validate-tests.js +0 -109
  204. package/framework/hooks/agent-teams/dispatch.js +0 -67
  205. package/framework/hooks/agent-teams/phase-advanced.js +0 -80
  206. package/framework/hooks/agent-teams/task-completed.js +0 -76
  207. package/framework/hooks/agent-teams/teammate-idle.js +0 -70
  208. package/src/commands/agents/agents-fuse.js +0 -97
  209. package/src/commands/agents/micro-agent.js +0 -112
  210. package/src/commands/agents/spawn-team.js +0 -237
  211. package/src/commands/agents/squad-template.js +0 -146
  212. package/src/commands/analytics/analytics.js +0 -176
  213. package/src/commands/context/context-prime.js +0 -63
  214. package/src/commands/context/core-four.js +0 -54
  215. package/src/commands/generation/generate-context.js +0 -40
  216. package/src/commands/project/detect-agents.js +0 -207
  217. package/src/commands/project/detect-workflow.js +0 -174
  218. package/src/commands/threads/thread-template.js +0 -103
  219. package/src/commands/threads/threads.js +0 -261
  220. package/src/commands/utils/session-summary.js +0 -291
  221. package/src/llm/analyzer.js +0 -215
  222. package/src/llm/few-shot-examples.js +0 -216
  223. package/src/llm/project-config-schema.json +0 -188
  224. package/src/llm/prompt-builder.js +0 -96
  225. /package/.morph/{project/context → context}/README.md +0 -0
  226. /package/.morph/{config → framework}/agents.json +0 -0
  227. /package/.morph/{standards → framework/standards}/ai-agents/blazor-ui.md +0 -0
  228. /package/.morph/{standards → framework/standards}/ai-agents/production.md +0 -0
  229. /package/.morph/{standards → framework/standards}/ai-agents/setup.md +0 -0
  230. /package/.morph/{standards → framework/standards}/ai-agents/workflows.md +0 -0
  231. /package/.morph/{standards → framework/standards}/architecture/ddd/aggregates.md +0 -0
  232. /package/.morph/{standards → framework/standards}/architecture/ddd/entities.md +0 -0
  233. /package/.morph/{standards → framework/standards}/architecture/ddd/value-objects.md +0 -0
  234. /package/.morph/{standards → framework/standards}/backend/api/minimal-api.md +0 -0
  235. /package/.morph/{standards → framework/standards}/backend/api/rest.md +0 -0
  236. /package/.morph/{standards → framework/standards}/backend/api/validation.md +0 -0
  237. /package/.morph/{standards → framework/standards}/backend/authentication/passkeys.md +0 -0
  238. /package/.morph/{standards → framework/standards}/backend/database/ef-core.md +0 -0
  239. /package/.morph/{standards → framework/standards}/backend/database/migrations.md +0 -0
  240. /package/.morph/{standards → framework/standards}/backend/database/postgresql/database.md +0 -0
  241. /package/.morph/{standards → framework/standards}/backend/database/repository-patterns.md +0 -0
  242. /package/.morph/{standards → framework/standards}/backend/database/vector-search-rag.md +0 -0
  243. /package/.morph/{standards → framework/standards}/backend/dotnet/async.md +0 -0
  244. /package/.morph/{standards → framework/standards}/backend/dotnet/core.md +0 -0
  245. /package/.morph/{standards → framework/standards}/backend/dotnet/di.md +0 -0
  246. /package/.morph/{standards → framework/standards}/backend/dotnet/program-cs-checklist.md +0 -0
  247. /package/.morph/{standards → framework/standards}/backend/integrations/asaas/asaas-api.md +0 -0
  248. /package/.morph/{standards → framework/standards}/backend/integrations/clerk/clerk-auth.md +0 -0
  249. /package/.morph/{standards → framework/standards}/backend/integrations/hangfire/hangfire-jobs.md +0 -0
  250. /package/.morph/{standards → framework/standards}/backend/integrations/resend/resend-email.md +0 -0
  251. /package/.morph/{standards → framework/standards}/context/analytics.md +0 -0
  252. /package/.morph/{standards → framework/standards}/context/bundles.md +0 -0
  253. /package/.morph/{standards → framework/standards}/context/priming.md +0 -0
  254. /package/.morph/{standards → framework/standards}/core/architecture.md +0 -0
  255. /package/.morph/{standards → framework/standards}/core/coding.md +0 -0
  256. /package/.morph/{standards → framework/standards}/core/git-branching-strategy.md +0 -0
  257. /package/.morph/{standards → framework/standards}/core/git.md +0 -0
  258. /package/.morph/{standards → framework/standards}/core/testing.md +0 -0
  259. /package/.morph/{standards → framework/standards}/data/nosql/blob-storage.md +0 -0
  260. /package/.morph/{standards → framework/standards}/data/nosql/cache/redis.md +0 -0
  261. /package/.morph/{standards → framework/standards}/data/nosql/cosmos-db.md +0 -0
  262. /package/.morph/{standards → framework/standards}/data/vector-search/azure-ai-search.md +0 -0
  263. /package/.morph/{standards → framework/standards}/data/vector-search/rag-chunking.md +0 -0
  264. /package/.morph/{standards → framework/standards}/frontend/blazor/design-checklist.md +0 -0
  265. /package/.morph/{standards → framework/standards}/frontend/blazor/fluent-ui-setup.md +0 -0
  266. /package/.morph/{standards → framework/standards}/frontend/blazor/fluent-ui.md +0 -0
  267. /package/.morph/{standards → framework/standards}/frontend/blazor/html-conversion.md +0 -0
  268. /package/.morph/{standards → framework/standards}/frontend/blazor/lifecycle.md +0 -0
  269. /package/.morph/{standards → framework/standards}/frontend/blazor/pitfalls.md +0 -0
  270. /package/.morph/{standards → framework/standards}/frontend/blazor/state.md +0 -0
  271. /package/.morph/{standards → framework/standards}/frontend/design-system/animations.md +0 -0
  272. /package/.morph/{standards → framework/standards}/frontend/design-system/naming.md +0 -0
  273. /package/.morph/{standards → framework/standards}/frontend/nextjs/nextjs-patterns.md +0 -0
  274. /package/.morph/{standards → framework/standards}/infrastructure/azure/azure.md +0 -0
  275. /package/.morph/{standards → framework/standards}/infrastructure/azure/bicep/bicep-patterns.md +0 -0
  276. /package/.morph/{standards → framework/standards}/infrastructure/azure/devops/azure-devops-setup.md +0 -0
  277. /package/.morph/{standards → framework/standards}/infrastructure/azure/devops/local-development.md +0 -0
  278. /package/.morph/{standards → framework/standards}/infrastructure/azure/services/functions.md +0 -0
  279. /package/.morph/{standards → framework/standards}/infrastructure/azure/services/service-bus.md +0 -0
  280. /package/.morph/{standards → framework/standards}/infrastructure/azure/services/storage.md +0 -0
  281. /package/.morph/{standards → framework/standards}/infrastructure/docker/easypanel-deploy.md +0 -0
  282. /package/.morph/{standards → framework/standards}/infrastructure/supabase/mcp-setup.md +0 -0
  283. /package/.morph/{standards → framework/standards}/infrastructure/supabase/supabase-auth.md +0 -0
  284. /package/.morph/{standards → framework/standards}/infrastructure/supabase/supabase-pgvector.md +0 -0
  285. /package/.morph/{standards → framework/standards}/infrastructure/supabase/supabase-rls.md +0 -0
  286. /package/.morph/{standards → framework/standards}/infrastructure/supabase/supabase-storage.md +0 -0
  287. /package/.morph/{standards → framework/standards}/integration/api/graphql.md +0 -0
  288. /package/.morph/{standards → framework/standards}/integration/api/grpc.md +0 -0
  289. /package/.morph/{standards → framework/standards}/integration/api/rest-design.md +0 -0
  290. /package/.morph/{standards → framework/standards}/integration/event-driven/cqrs.md +0 -0
  291. /package/.morph/{standards → framework/standards}/integration/event-driven/event-sourcing.md +0 -0
  292. /package/.morph/{standards → framework/standards}/integration/event-driven/service-bus.md +0 -0
  293. /package/.morph/{standards → framework/standards}/observability/logging.md +0 -0
  294. /package/.morph/{standards → framework/standards}/observability/metrics.md +0 -0
  295. /package/.morph/{standards → framework/standards}/observability/monitoring.md +0 -0
  296. /package/.morph/{standards → framework/standards}/observability/tracing.md +0 -0
  297. /package/.morph/{standards → framework/standards}/workflows/parallel-execution.md +0 -0
  298. /package/.morph/{standards → framework/standards}/workflows/thread-management.md +0 -0
  299. /package/.morph/{templates → framework/templates}/.idea/morph-templates.xml +0 -0
  300. /package/.morph/{templates → framework/templates}/.vscode/morph-templates.code-snippets +0 -0
  301. /package/.morph/{templates → framework/templates}/IDE-SNIPPETS.md +0 -0
  302. /package/.morph/{templates → framework/templates}/code/dotnet/backend/repository.cs +0 -0
  303. /package/.morph/{templates → framework/templates}/code/dotnet/backend/service.cs +0 -0
  304. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/Commands.cs +0 -0
  305. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/Entities.cs +0 -0
  306. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/Queries.cs +0 -0
  307. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/README.md +0 -0
  308. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/api-contracts.cs +0 -0
  309. /package/.morph/{templates → framework/templates}/code/dotnet/contracts/contracts.cs +0 -0
  310. /package/.morph/{templates → framework/templates}/code/dotnet/database/migration.cs +0 -0
  311. /package/.morph/{templates → framework/templates}/code/dotnet/frontend/component.razor +0 -0
  312. /package/.morph/{templates → framework/templates}/code/dotnet/jobs/agent.cs +0 -0
  313. /package/.morph/{templates → framework/templates}/code/dotnet/jobs/job.cs +0 -0
  314. /package/.morph/{templates → framework/templates}/code/dotnet/test.cs +0 -0
  315. /package/.morph/{templates → framework/templates}/code/sql/rls-policy.sql +0 -0
  316. /package/.morph/{templates → framework/templates}/code/sql/supabase-migration.sql +0 -0
  317. /package/.morph/{templates → framework/templates}/code/sql/supabase-migration.template.sql +0 -0
  318. /package/.morph/{templates → framework/templates}/code/typescript/contracts.ts +0 -0
  319. /package/.morph/{templates → framework/templates}/docs/proposal.md +0 -0
  320. /package/.morph/{templates → framework/templates}/examples/design-system-examples.md +0 -0
  321. /package/.morph/{templates → framework/templates}/feature/decisions.md +0 -0
  322. /package/.morph/{templates → framework/templates}/feature/recap.md +0 -0
  323. /package/.morph/{templates → framework/templates}/feature/tasks.md +0 -0
  324. /package/.morph/{templates → framework/templates}/infrastructure/azure/Dockerfile.example +0 -0
  325. /package/.morph/{templates → framework/templates}/infrastructure/azure/README.md +0 -0
  326. /package/.morph/{templates → framework/templates}/infrastructure/azure/app-insights.bicep +0 -0
  327. /package/.morph/{templates → framework/templates}/infrastructure/azure/app-service.bicep +0 -0
  328. /package/.morph/{templates → framework/templates}/infrastructure/azure/container-app-env.bicep +0 -0
  329. /package/.morph/{templates → framework/templates}/infrastructure/azure/container-app.bicep +0 -0
  330. /package/.morph/{templates → framework/templates}/infrastructure/azure/deploy-checklist.md +0 -0
  331. /package/.morph/{templates → framework/templates}/infrastructure/azure/deploy.ps1 +0 -0
  332. /package/.morph/{templates → framework/templates}/infrastructure/azure/deploy.sh +0 -0
  333. /package/.morph/{templates → framework/templates}/infrastructure/azure/key-vault.bicep +0 -0
  334. /package/.morph/{templates → framework/templates}/infrastructure/azure/main.bicep +0 -0
  335. /package/.morph/{templates → framework/templates}/infrastructure/azure/parameters.dev.json +0 -0
  336. /package/.morph/{templates → framework/templates}/infrastructure/azure/parameters.prod.json +0 -0
  337. /package/.morph/{templates → framework/templates}/infrastructure/azure/parameters.staging.json +0 -0
  338. /package/.morph/{templates → framework/templates}/infrastructure/azure/sql-database.bicep +0 -0
  339. /package/.morph/{templates → framework/templates}/infrastructure/azure/storage.bicep +0 -0
  340. /package/.morph/{templates → framework/templates}/infrastructure/docker/Dockerfile.template +0 -0
  341. /package/.morph/{templates → framework/templates}/infrastructure/docker/docker-compose.template.yml +0 -0
  342. /package/.morph/{templates → framework/templates}/infrastructure/docker/dockerfile-api.dockerfile +0 -0
  343. /package/.morph/{templates → framework/templates}/infrastructure/docker/dockerfile-web.dockerfile +0 -0
  344. /package/.morph/{templates → framework/templates}/infrastructure/docker/easypanel.template.json +0 -0
  345. /package/.morph/{templates → framework/templates}/infrastructure/github/actions/azure-auth/action.yml.hbs +0 -0
  346. /package/.morph/{templates → framework/templates}/infrastructure/github/actions/docker-build-push/action.yml.hbs +0 -0
  347. /package/.morph/{templates → framework/templates}/infrastructure/github/actions/health-check/action.yml.hbs +0 -0
  348. /package/.morph/{templates → framework/templates}/infrastructure/github/workflows/deploy-easypanel.yml.hbs +0 -0
  349. /package/.morph/{templates → framework/templates}/infrastructure/github/workflows/docker-build-push.yml.hbs +0 -0
  350. /package/.morph/{templates → framework/templates}/infrastructure/github/workflows/dotnet-build.yml.hbs +0 -0
  351. /package/.morph/{templates → framework/templates}/integrations/asaas-client.cs +0 -0
  352. /package/.morph/{templates → framework/templates}/integrations/asaas-webhook.cs +0 -0
  353. /package/.morph/{templates → framework/templates}/integrations/azure-identity-config.cs +0 -0
  354. /package/.morph/{templates → framework/templates}/integrations/clerk-config.cs +0 -0
  355. /package/.morph/{templates → framework/templates}/meta-prompts/fusion/fusion-agent.md +0 -0
  356. /package/.morph/{templates → framework/templates}/meta-prompts/fusion/fusion-aggregator.md +0 -0
  357. /package/.morph/{templates → framework/templates}/meta-prompts/hops/hop-retry.md +0 -0
  358. /package/.morph/{templates → framework/templates}/meta-prompts/hops/hop-validation.md +0 -0
  359. /package/.morph/{templates → framework/templates}/meta-prompts/hops/hop-wrapper.md +0 -0
  360. /package/.morph/{templates → framework/templates}/meta-prompts/parallel-workers/parallel-coordinator.md +0 -0
  361. /package/.morph/{templates → framework/templates}/meta-prompts/squad-leaders/backend-squad.md +0 -0
  362. /package/.morph/{templates → framework/templates}/meta-prompts/squad-leaders/frontend-squad.md +0 -0
  363. /package/.morph/{templates → framework/templates}/meta-prompts/squad-leaders/squad-leader.md +0 -0
  364. /package/.morph/{templates → framework/templates}/meta-prompts/validators/checkpoint-validator.md +0 -0
  365. /package/.morph/{templates → framework/templates}/saas/subscription.cs +0 -0
  366. /package/.morph/{templates → framework/templates}/saas/tenant.cs +0 -0
  367. /package/.morph/{templates → framework/templates}/state.template.json +0 -0
  368. /package/.morph/{templates → framework/templates}/ui/FluentDesignTheme.cs +0 -0
  369. /package/.morph/{templates → framework/templates}/ui/MudTheme.cs +0 -0
  370. /package/.morph/{templates → framework/templates}/ui/design-system.css +0 -0
  371. /package/framework/hooks/{commit-msg → git/commit-msg}/conventional-commits.sh +0 -0
  372. /package/framework/hooks/{pre-commit → git/pre-commit}/agents.sh +0 -0
  373. /package/framework/hooks/{pre-commit → git/pre-commit}/orchestrator.sh +0 -0
  374. /package/framework/hooks/{pre-commit → git/pre-commit}/specs.sh +0 -0
  375. /package/framework/hooks/{pre-push → git/pre-push}/run-tests.sh +0 -0
@@ -1,592 +1,831 @@
1
- /**
2
- * MORPH-SPEC State Manager Library
3
- *
4
- * Manages state.json for tracking features, progress, agents, and checkpoints.
5
- * Used both by CLI commands and internal automation.
6
- */
7
-
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
- import { join, dirname } from 'path';
10
- import { detectWorkflow } from '../workflows/workflow-detector.js';
11
-
12
- const STATE_FILE_NAME = '.morph/state.json';
13
-
14
- // ============================================================================
15
- // Core Functions
16
- // ============================================================================
17
-
18
- /**
19
- * Get the state file path (looks in current working directory)
20
- */
21
- export function getStatePath() {
22
- return join(process.cwd(), STATE_FILE_NAME);
23
- }
24
-
25
- /**
26
- * Check if state file exists
27
- */
28
- export function stateExists() {
29
- return existsSync(getStatePath());
30
- }
31
-
32
- /**
33
- * Load state from disk
34
- * @param {boolean} throwOnError - If false, returns null instead of throwing
35
- * @returns {Object|null} State object or null
36
- */
37
- export function loadState(throwOnError = true) {
38
- const statePath = getStatePath();
39
-
40
- if (!existsSync(statePath)) {
41
- if (throwOnError) {
42
- throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
43
- }
44
- return null;
45
- }
46
-
47
- try {
48
- const content = readFileSync(statePath, 'utf8');
49
- return JSON.parse(content);
50
- } catch (err) {
51
- if (throwOnError) {
52
- throw new Error(`Failed to parse state.json: ${err.message}`);
53
- }
54
- return null;
55
- }
56
- }
57
-
58
- /**
59
- * Save state to disk
60
- * @param {Object} state - State object to save
61
- */
62
- export function saveState(state) {
63
- state.metadata = state.metadata || {};
64
- state.metadata.lastUpdated = new Date().toISOString();
65
-
66
- const statePath = getStatePath();
67
- const stateDir = dirname(statePath);
68
-
69
- // Ensure .morph directory exists
70
- if (!existsSync(stateDir)) {
71
- mkdirSync(stateDir, { recursive: true });
72
- }
73
-
74
- writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
75
- }
76
-
77
- /**
78
- * Initialize new state file
79
- * @param {Object} options - Options
80
- * @param {boolean} options.force - Overwrite existing file
81
- * @param {string} options.projectName - Project name
82
- * @param {string} options.projectType - Project type (e.g., 'blazor-server')
83
- * @returns {Object} Initial state
84
- */
85
- export function initState(options = {}) {
86
- const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
87
-
88
- if (stateExists() && !force) {
89
- throw new Error('State file already exists. Use force=true to overwrite.');
90
- }
91
-
92
- const initialState = {
93
- version: "3.0.0",
94
- project: {
95
- name: projectName,
96
- type: projectType,
97
- createdAt: new Date().toISOString(),
98
- updatedAt: new Date().toISOString()
99
- },
100
- features: {},
101
- threads: {},
102
- metadata: {
103
- totalFeatures: 0,
104
- completedFeatures: 0,
105
- totalTimeSpent: 0,
106
- lastUpdated: new Date().toISOString()
107
- }
108
- };
109
-
110
- saveState(initialState);
111
- return initialState;
112
- }
113
-
114
- // ============================================================================
115
- // Feature Operations
116
- // ============================================================================
117
-
118
- /**
119
- * Get feature from state
120
- * @param {string} featureName - Feature name
121
- * @returns {Object|null} Feature object or null
122
- */
123
- export function getFeature(featureName) {
124
- const state = loadState();
125
- return state.features[featureName] || null;
126
- }
127
-
128
- /**
129
- * Create or get feature with default structure
130
- * @param {string} featureName - Feature name
131
- * @param {Object} options - Options
132
- * @param {string} options.userRequest - User's feature request for workflow detection
133
- * @param {string} options.workflow - Manual workflow override
134
- * @param {string} options.projectPath - Project path
135
- * @returns {Promise<Object>} Feature object
136
- */
137
- async function ensureFeature(featureName, options = {}) {
138
- const state = loadState();
139
-
140
- if (!state.features[featureName]) {
141
- // Detect workflow if userRequest provided
142
- let workflowId = 'auto';
143
- let workflowDetection = {
144
- auto: false,
145
- confidence: 0,
146
- userOverride: false
147
- };
148
-
149
- if (options.workflow) {
150
- // Manual override
151
- workflowId = options.workflow;
152
- workflowDetection.userOverride = true;
153
- } else if (options.userRequest) {
154
- // Auto-detect
155
- try {
156
- const detection = await detectWorkflow({
157
- userRequest: options.userRequest,
158
- projectPath: options.projectPath || '.',
159
- featureName
160
- });
161
- workflowId = detection.workflowId;
162
- workflowDetection.auto = true;
163
- workflowDetection.confidence = detection.confidence;
164
- workflowDetection.matchedKeywords = detection.matchedKeywords;
165
- workflowDetection.estimatedComplexity = detection.estimatedComplexity;
166
- workflowDetection.reasoning = detection.reasoning;
167
- } catch (err) {
168
- // If detection fails, fall back to auto
169
- console.warn('Workflow detection failed:', err.message);
170
- }
171
- }
172
-
173
- state.features[featureName] = {
174
- status: "draft",
175
- phase: "proposal",
176
- workflow: workflowId, // auto | fast-track | standard | full-morph | design-impl | ui-refresh
177
- workflowDetection,
178
- createdAt: new Date().toISOString(),
179
- updatedAt: new Date().toISOString(),
180
- activeAgents: [],
181
- approvalGates: {
182
- proposal: { approved: false, timestamp: null, approvedBy: null },
183
- uiux: { approved: false, timestamp: null, approvedBy: null },
184
- design: { approved: false, timestamp: null, approvedBy: null },
185
- tasks: { approved: false, timestamp: null, approvedBy: null }
186
- },
187
- outputs: {
188
- proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
189
- spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
190
- contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
191
- tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.md` },
192
- uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
193
- uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
194
- uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
195
- uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
196
- decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
197
- recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
198
- },
199
- tasks: {
200
- total: 0,
201
- completed: 0,
202
- inProgress: 0,
203
- pending: 0
204
- },
205
- checkpoints: [],
206
- threadMetrics: {
207
- totalThreads: 0,
208
- parallelPeak: 0,
209
- avgDuration: 0,
210
- checkpointPassRate: 100
211
- },
212
- trustConfig: {
213
- level: 'low',
214
- history: [],
215
- autoApprove: {
216
- design: false,
217
- tasks: false
218
- }
219
- },
220
- contextBundles: []
221
- };
222
-
223
- state.metadata.totalFeatures++;
224
- saveState(state);
225
- }
226
-
227
- return state.features[featureName];
228
- }
229
-
230
- /**
231
- * Update feature property (supports nested keys like "tasks.completed")
232
- * @param {string} featureName - Feature name
233
- * @param {string} key - Property key (supports dot notation)
234
- * @param {any} value - Value to set
235
- */
236
- export async function updateFeature(featureName, key, value) {
237
- await ensureFeature(featureName);
238
- const state = loadState(); // Load AFTER ensuring feature exists
239
-
240
- const keys = key.split('.');
241
- let target = state.features[featureName];
242
-
243
- for (let i = 0; i < keys.length - 1; i++) {
244
- if (!target[keys[i]]) {
245
- target[keys[i]] = {};
246
- }
247
- target = target[keys[i]];
248
- }
249
-
250
- const finalKey = keys[keys.length - 1];
251
- target[finalKey] = value;
252
- state.features[featureName].updatedAt = new Date().toISOString();
253
-
254
- saveState(state);
255
- }
256
-
257
- /**
258
- * Update multiple feature properties at once
259
- * @param {string} featureName - Feature name
260
- * @param {Object} updates - Object with key-value pairs to update
261
- */
262
- export async function updateFeatureMultiple(featureName, updates) {
263
- await ensureFeature(featureName);
264
- const state = loadState(); // Load AFTER ensuring feature exists
265
-
266
- for (const [key, value] of Object.entries(updates)) {
267
- const keys = key.split('.');
268
- let target = state.features[featureName];
269
-
270
- for (let i = 0; i < keys.length - 1; i++) {
271
- if (!target[keys[i]]) {
272
- target[keys[i]] = {};
273
- }
274
- target = target[keys[i]];
275
- }
276
-
277
- const finalKey = keys[keys.length - 1];
278
- target[finalKey] = value;
279
- }
280
-
281
- state.features[featureName].updatedAt = new Date().toISOString();
282
- saveState(state);
283
- }
284
-
285
- /**
286
- * Add checkpoint to feature
287
- * @param {string} featureName - Feature name
288
- * @param {string} note - Checkpoint note
289
- * @returns {Object} Checkpoint object
290
- */
291
- export async function addCheckpoint(featureName, note) {
292
- await ensureFeature(featureName);
293
- const state = loadState();
294
- const feature = state.features[featureName];
295
-
296
- const checkpoint = {
297
- passed: true,
298
- checkpointNum: feature.checkpoints.length + 1,
299
- timestamp: new Date().toISOString(),
300
- phase: feature.phase,
301
- results: [],
302
- summary: {
303
- note: note,
304
- completedTasks: feature.tasks.completed
305
- }
306
- };
307
-
308
- feature.checkpoints.push(checkpoint);
309
- feature.updatedAt = new Date().toISOString();
310
-
311
- saveState(state);
312
- return checkpoint;
313
- }
314
-
315
- /**
316
- * Add agent to feature
317
- * @param {string} featureName - Feature name
318
- * @param {string} agentId - Agent ID
319
- * @returns {boolean} True if added, false if already exists
320
- */
321
- export async function addAgent(featureName, agentId) {
322
- await ensureFeature(featureName);
323
- const state = loadState(); // Load AFTER ensuring feature exists
324
-
325
- if (!state.features[featureName].activeAgents.includes(agentId)) {
326
- state.features[featureName].activeAgents.push(agentId);
327
- state.features[featureName].updatedAt = new Date().toISOString();
328
- saveState(state);
329
- return true;
330
- }
331
-
332
- return false;
333
- }
334
-
335
- /**
336
- * Remove agent from feature
337
- * @param {string} featureName - Feature name
338
- * @param {string} agentId - Agent ID
339
- * @returns {boolean} True if removed, false if not found
340
- */
341
- export function removeAgent(featureName, agentId) {
342
- const state = loadState();
343
-
344
- if (!state.features[featureName]) {
345
- throw new Error(`Feature '${featureName}' not found.`);
346
- }
347
-
348
- const index = state.features[featureName].activeAgents.indexOf(agentId);
349
- if (index > -1) {
350
- state.features[featureName].activeAgents.splice(index, 1);
351
- state.features[featureName].updatedAt = new Date().toISOString();
352
- saveState(state);
353
- return true;
354
- }
355
-
356
- return false;
357
- }
358
-
359
- /**
360
- * Normalize output type from kebab-case to camelCase for UI types (BUG #12 fix)
361
- * @param {string} type - Output type (e.g., 'ui-design-system' or 'uiDesignSystem')
362
- * @returns {string} Normalized type in camelCase
363
- */
364
- function normalizeOutputType(type) {
365
- const kebabMap = {
366
- 'ui-design-system': 'uiDesignSystem',
367
- 'ui-mockups': 'uiMockups',
368
- 'ui-components': 'uiComponents',
369
- 'ui-flows': 'uiFlows'
370
- };
371
- return kebabMap[type] || type;
372
- }
373
-
374
- /**
375
- * Mark output as created
376
- * @param {string} featureName - Feature name
377
- * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
378
- */
379
- export async function markOutput(featureName, outputType) {
380
- await ensureFeature(featureName);
381
- const state = loadState();
382
-
383
- const normalized = normalizeOutputType(outputType);
384
-
385
- if (!state.features[featureName].outputs[normalized]) {
386
- throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem (or ui-design-system), uiMockups (or ui-mockups), uiComponents (or ui-components), uiFlows (or ui-flows), decisions, recap`);
387
- }
388
-
389
- state.features[featureName].outputs[normalized].created = true;
390
- state.features[featureName].updatedAt = new Date().toISOString();
391
-
392
- // If marking tasks output, try to sync task count from state tasks array
393
- if (normalized === 'tasks') {
394
- syncTasksCount(state.features[featureName]);
395
- }
396
-
397
- saveState(state);
398
- }
399
-
400
- /**
401
- * Sync progress counters from taskList array into feature.progress
402
- * @param {Object} feature - Feature object
403
- */
404
- function syncTasksCount(feature) {
405
- const list = feature.taskList;
406
- if (!Array.isArray(list) || list.length === 0) return;
407
- const completed = list.filter(t => t.status === 'completed').length;
408
- feature.progress = {
409
- total: list.length,
410
- completed,
411
- inProgress: list.filter(t => t.status === 'in_progress').length,
412
- pending: list.filter(t => t.status === 'pending').length,
413
- percentage: Math.round((completed / list.length) * 100)
414
- };
415
- }
416
-
417
- /**
418
- * Skip a phase and record it
419
- * @param {string} featureName - Feature name
420
- * @param {string} phase - Phase to skip
421
- * @param {string} reason - Reason for skipping
422
- */
423
- export async function skipPhase(featureName, phase, reason = '') {
424
- await ensureFeature(featureName);
425
- const state = loadState();
426
-
427
- if (!state.features[featureName].skippedPhases) {
428
- state.features[featureName].skippedPhases = [];
429
- }
430
-
431
- // Don't add duplicates
432
- const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
433
- if (!existing) {
434
- state.features[featureName].skippedPhases.push({
435
- phase,
436
- reason,
437
- timestamp: new Date().toISOString()
438
- });
439
- }
440
-
441
- state.features[featureName].updatedAt = new Date().toISOString();
442
- saveState(state);
443
- }
444
-
445
- /**
446
- * Get skipped phases for a feature
447
- * @param {string} featureName - Feature name
448
- * @returns {Array} Array of skipped phases
449
- */
450
- export function getSkippedPhases(featureName) {
451
- const state = loadState();
452
- if (!state.features[featureName]) {
453
- return [];
454
- }
455
- return state.features[featureName].skippedPhases || [];
456
- }
457
-
458
- /**
459
- * List all features
460
- * @returns {Array} Array of [featureName, featureObject] tuples
461
- */
462
- export function listFeatures() {
463
- const state = loadState();
464
- return Object.entries(state.features);
465
- }
466
-
467
- /**
468
- * Get project summary
469
- * @returns {Object} Summary with metadata
470
- */
471
- export function getSummary() {
472
- const state = loadState();
473
- return {
474
- project: state.project,
475
- metadata: state.metadata,
476
- featuresCount: Object.keys(state.features).length
477
- };
478
- }
479
-
480
- // ============================================================================
481
- // Approval Gates Operations
482
- // ============================================================================
483
-
484
- /**
485
- * Set approval gate status
486
- * @param {string} featureName - Feature name
487
- * @param {string} gate - Gate name (proposal, uiux, design, tasks)
488
- * @param {boolean} approved - Approval status
489
- * @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
490
- */
491
- export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
492
- const state = loadState();
493
- const feature = await ensureFeature(featureName);
494
-
495
- if (!feature.approvalGates) {
496
- feature.approvalGates = {
497
- proposal: { approved: false, timestamp: null, approvedBy: null },
498
- uiux: { approved: false, timestamp: null, approvedBy: null },
499
- design: { approved: false, timestamp: null, approvedBy: null },
500
- tasks: { approved: false, timestamp: null, approvedBy: null }
501
- };
502
- }
503
-
504
- feature.approvalGates[gate] = {
505
- approved,
506
- timestamp: metadata.approvedAt || metadata.rejectedAt || new Date().toISOString(),
507
- approvedBy: metadata.approvedBy || metadata.rejectedBy || null,
508
- ...metadata
509
- };
510
-
511
- state.features[featureName] = feature;
512
- saveState(state);
513
- }
514
-
515
- /**
516
- * Get approval gate status
517
- * @param {string} featureName - Feature name
518
- * @param {string} gate - Gate name
519
- * @returns {Object|null} Gate object or null
520
- */
521
- export function getApprovalGate(featureName, gate) {
522
- const state = loadState();
523
- const feature = state.features[featureName];
524
-
525
- if (!feature || !feature.approvalGates) {
526
- return null;
527
- }
528
-
529
- return feature.approvalGates[gate] || null;
530
- }
531
-
532
- /**
533
- * Check if feature is pending approval
534
- * @param {string} featureName - Feature name
535
- * @returns {boolean} True if any gate is pending approval
536
- */
537
- export function isPendingApproval(featureName) {
538
- const state = loadState();
539
- const feature = state.features[featureName];
540
-
541
- if (!feature || !feature.approvalGates) {
542
- return false;
543
- }
544
-
545
- const currentPhase = feature.phase;
546
-
547
- // Check if current phase has an approval gate
548
- const phaseGateMap = {
549
- 'design': 'design',
550
- 'tasks': 'tasks',
551
- 'uiux': 'uiux'
552
- };
553
-
554
- const relevantGate = phaseGateMap[currentPhase];
555
- if (!relevantGate) {
556
- return false;
557
- }
558
-
559
- const gate = feature.approvalGates[relevantGate];
560
- return gate && !gate.approved;
561
- }
562
-
563
- /**
564
- * Get approval history for a feature
565
- * @param {string} featureName - Feature name
566
- * @returns {Array} Array of approval events
567
- */
568
- export function getApprovalHistory(featureName) {
569
- const state = loadState();
570
- const feature = state.features[featureName];
571
-
572
- if (!feature || !feature.approvalGates) {
573
- return [];
574
- }
575
-
576
- const history = [];
577
-
578
- Object.entries(feature.approvalGates).forEach(([gate, data]) => {
579
- if (data.timestamp) {
580
- history.push({
581
- gate,
582
- approved: data.approved,
583
- timestamp: data.timestamp,
584
- approvedBy: data.approvedBy || data.rejectedBy,
585
- reason: data.reason
586
- });
587
- }
588
- });
589
-
590
- // Sort by timestamp
591
- return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
592
- }
1
+ /**
2
+ * MORPH-SPEC State Manager Library
3
+ *
4
+ * Manages state.json for tracking features, progress, agents, and checkpoints.
5
+ * Used both by CLI commands and internal automation.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { detectWorkflow } from '../workflows/workflow-detector.js';
11
+ import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
12
+
13
+ const STATE_FILE_NAME = '.morph/state.json';
14
+
15
+ // ============================================================================
16
+ // Core Functions
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Get the state file path (looks in current working directory)
21
+ */
22
+ export function getStatePath() {
23
+ return join(process.cwd(), STATE_FILE_NAME);
24
+ }
25
+ /**
26
+ * Derive current phase from filesystem — checks for phase folders in descending order.
27
+ * Returns the phase corresponding to the highest-numbered folder present.
28
+ *
29
+ * @param {string} featurePath - Absolute path to .morph/features/{feature}/
30
+ * @returns {string} Phase name: 'implement' | 'tasks' | 'uiux' | 'design' | 'proposal' | 'setup'
31
+ */
32
+ export function derivePhase(featurePath) {
33
+ const phaseMap = [
34
+ ['4-implement', 'implement'],
35
+ ['3-tasks', 'tasks'],
36
+ ['2-ui', 'uiux'],
37
+ ['1-design', 'design'],
38
+ ['0-proposal', 'proposal'],
39
+ ];
40
+ for (const [folder, phase] of phaseMap) {
41
+ if (existsSync(join(featurePath, folder))) return phase;
42
+ }
43
+ return 'setup';
44
+ }
45
+
46
+ /**
47
+ * Derive output existence from filesystem — checks if each output file exists at its expected path.
48
+ * Returns an object matching the old outputs shape for backwards-compatible display.
49
+ *
50
+ * @param {string} featureName - Feature name
51
+ * @param {string} [baseDir] - Project base dir (defaults to cwd)
52
+ * @returns {Object} Map of outputType -> { created: boolean, path: string }
53
+ */
54
+ export function deriveOutputs(featureName, baseDir = process.cwd()) {
55
+ const outputPaths = getAllOutputPaths(featureName);
56
+ const result = {};
57
+ for (const [type, { path: relPath }] of Object.entries(outputPaths)) {
58
+ const absPath = join(baseDir, relPath);
59
+ result[type] = { created: existsSync(absPath), path: relPath };
60
+ }
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Check if state file exists
66
+ */
67
+ export function stateExists() {
68
+ return existsSync(getStatePath());
69
+ }
70
+
71
+ /**
72
+ * Load state from disk
73
+ * @param {boolean} throwOnError - If false, returns null instead of throwing
74
+ * @returns {Object|null} State object or null
75
+ */
76
+ export function loadState(throwOnError = true) {
77
+ const statePath = getStatePath();
78
+
79
+ if (!existsSync(statePath)) {
80
+ if (throwOnError) {
81
+ throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
82
+ }
83
+ return null;
84
+ }
85
+
86
+ try {
87
+ const content = readFileSync(statePath, 'utf8');
88
+ const state = JSON.parse(content);
89
+
90
+ // Migrate v3.0.0 → v4.0.0 (new directory layout)
91
+ if (state.version === '3.0.0') {
92
+ state.version = '4.0.0';
93
+ for (const [name, feature] of Object.entries(state.features || {})) {
94
+ if (!feature.outputs) continue;
95
+ const outputPathMap = getAllOutputPaths(name);
96
+ for (const [key, { path: newPath }] of Object.entries(outputPathMap)) {
97
+ if (feature.outputs[key]) {
98
+ feature.outputs[key].path = newPath;
99
+ }
100
+ }
101
+ }
102
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
103
+ }
104
+
105
+ // Migrate v4.x -> v5.0.0: remove outputs and phase (now derived from filesystem)
106
+ if (state.version && state.version.startsWith('4.')) {
107
+ state.version = '5.0.0';
108
+ for (const feature of Object.values(state.features || {})) {
109
+ delete feature.outputs;
110
+ delete feature.phase;
111
+ }
112
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
113
+ }
114
+
115
+ return state;
116
+ } catch (err) {
117
+ if (throwOnError) {
118
+ throw new Error(`Failed to parse state.json: ${err.message}`);
119
+ }
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Save state to disk
126
+ * @param {Object} state - State object to save
127
+ */
128
+ export function saveState(state) {
129
+ state.metadata = state.metadata || {};
130
+ state.metadata.lastUpdated = new Date().toISOString();
131
+
132
+ const statePath = getStatePath();
133
+ const stateDir = dirname(statePath);
134
+
135
+ // Ensure .morph directory exists
136
+ if (!existsSync(stateDir)) {
137
+ mkdirSync(stateDir, { recursive: true });
138
+ }
139
+
140
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
141
+ }
142
+
143
+ /**
144
+ * Initialize new state file
145
+ * @param {Object} options - Options
146
+ * @param {boolean} options.force - Overwrite existing file
147
+ * @param {string} options.projectName - Project name
148
+ * @param {string} options.projectType - Project type (e.g., 'blazor-server')
149
+ * @returns {Object} Initial state
150
+ */
151
+ export function initState(options = {}) {
152
+ const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
153
+
154
+ if (stateExists() && !force) {
155
+ throw new Error('State file already exists. Use force=true to overwrite.');
156
+ }
157
+
158
+ const initialState = {
159
+ version: "5.0.0",
160
+ project: {
161
+ name: projectName,
162
+ type: projectType,
163
+ createdAt: new Date().toISOString(),
164
+ updatedAt: new Date().toISOString()
165
+ },
166
+ features: {},
167
+ threads: {},
168
+ metadata: {
169
+ totalFeatures: 0,
170
+ completedFeatures: 0,
171
+ totalTimeSpent: 0,
172
+ lastUpdated: new Date().toISOString()
173
+ }
174
+ };
175
+
176
+ saveState(initialState);
177
+ return initialState;
178
+ }
179
+
180
+ // ============================================================================
181
+ // Feature Operations
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Get feature from state
186
+ * @param {string} featureName - Feature name
187
+ * @returns {Object|null} Feature object or null
188
+ */
189
+ export function getFeature(featureName) {
190
+ const state = loadState();
191
+ return state.features[featureName] || null;
192
+ }
193
+
194
+ /**
195
+ * Create or get feature with default structure
196
+ * @param {string} featureName - Feature name
197
+ * @param {Object} options - Options
198
+ * @param {string} options.userRequest - User's feature request for workflow detection
199
+ * @param {string} options.workflow - Manual workflow override
200
+ * @param {string} options.projectPath - Project path
201
+ * @returns {Promise<Object>} Feature object
202
+ */
203
+ async function ensureFeature(featureName, options = {}) {
204
+ const state = loadState();
205
+
206
+ if (!state.features[featureName]) {
207
+ // Detect workflow if userRequest provided
208
+ let workflowId = 'auto';
209
+ let workflowDetection = {
210
+ auto: false,
211
+ confidence: 0,
212
+ userOverride: false
213
+ };
214
+
215
+ if (options.workflow) {
216
+ // Manual override
217
+ workflowId = options.workflow;
218
+ workflowDetection.userOverride = true;
219
+ } else if (options.userRequest) {
220
+ // Auto-detect
221
+ try {
222
+ const detection = await detectWorkflow({
223
+ userRequest: options.userRequest,
224
+ projectPath: options.projectPath || '.',
225
+ featureName
226
+ });
227
+ workflowId = detection.workflowId;
228
+ workflowDetection.auto = true;
229
+ workflowDetection.confidence = detection.confidence;
230
+ workflowDetection.matchedKeywords = detection.matchedKeywords;
231
+ workflowDetection.estimatedComplexity = detection.estimatedComplexity;
232
+ workflowDetection.reasoning = detection.reasoning;
233
+ } catch (err) {
234
+ // If detection fails, fall back to auto
235
+ console.warn('Workflow detection failed:', err.message);
236
+ }
237
+ }
238
+
239
+ state.features[featureName] = {
240
+ status: "draft",
241
+ workflow: workflowId, // auto | fast-track | standard | full-morph | design-impl | ui-refresh
242
+ workflowDetection,
243
+ createdAt: new Date().toISOString(),
244
+ updatedAt: new Date().toISOString(),
245
+ activeAgents: [],
246
+ approvalGates: {
247
+ proposal: { approved: false, timestamp: null, approvedBy: null },
248
+ uiux: { approved: false, timestamp: null, approvedBy: null },
249
+ design: { approved: false, timestamp: null, approvedBy: null },
250
+ tasks: { approved: false, timestamp: null, approvedBy: null }
251
+ },
252
+ tasks: {
253
+ total: 0,
254
+ completed: 0,
255
+ inProgress: 0,
256
+ pending: 0
257
+ },
258
+ checkpoints: [],
259
+ threadMetrics: {
260
+ totalThreads: 0,
261
+ parallelPeak: 0,
262
+ avgDuration: 0,
263
+ checkpointPassRate: 100
264
+ },
265
+ trustConfig: {
266
+ level: 'low',
267
+ history: [],
268
+ autoApprove: {
269
+ design: false,
270
+ tasks: false
271
+ }
272
+ },
273
+ contextBundles: [],
274
+ fileChanges: []
275
+ };
276
+
277
+ state.metadata.totalFeatures++;
278
+ saveState(state);
279
+ }
280
+
281
+ return state.features[featureName];
282
+ }
283
+
284
+ /**
285
+ * Update feature property (supports nested keys like "tasks.completed")
286
+ * @param {string} featureName - Feature name
287
+ * @param {string} key - Property key (supports dot notation)
288
+ * @param {any} value - Value to set
289
+ */
290
+ export async function updateFeature(featureName, key, value) {
291
+ await ensureFeature(featureName);
292
+ const state = loadState(); // Load AFTER ensuring feature exists
293
+
294
+ const keys = key.split('.');
295
+ let target = state.features[featureName];
296
+
297
+ for (let i = 0; i < keys.length - 1; i++) {
298
+ if (!target[keys[i]]) {
299
+ target[keys[i]] = {};
300
+ }
301
+ target = target[keys[i]];
302
+ }
303
+
304
+ const finalKey = keys[keys.length - 1];
305
+ target[finalKey] = value;
306
+ state.features[featureName].updatedAt = new Date().toISOString();
307
+
308
+ saveState(state);
309
+ }
310
+
311
+ /**
312
+ * Update multiple feature properties at once
313
+ * @param {string} featureName - Feature name
314
+ * @param {Object} updates - Object with key-value pairs to update
315
+ */
316
+ export async function updateFeatureMultiple(featureName, updates) {
317
+ await ensureFeature(featureName);
318
+ const state = loadState(); // Load AFTER ensuring feature exists
319
+
320
+ for (const [key, value] of Object.entries(updates)) {
321
+ const keys = key.split('.');
322
+ let target = state.features[featureName];
323
+
324
+ for (let i = 0; i < keys.length - 1; i++) {
325
+ if (!target[keys[i]]) {
326
+ target[keys[i]] = {};
327
+ }
328
+ target = target[keys[i]];
329
+ }
330
+
331
+ const finalKey = keys[keys.length - 1];
332
+ target[finalKey] = value;
333
+ }
334
+
335
+ state.features[featureName].updatedAt = new Date().toISOString();
336
+ saveState(state);
337
+ }
338
+
339
+ /**
340
+ * Add checkpoint to feature
341
+ * @param {string} featureName - Feature name
342
+ * @param {string} note - Checkpoint note
343
+ * @returns {Object} Checkpoint object
344
+ */
345
+ export async function addCheckpoint(featureName, note) {
346
+ await ensureFeature(featureName);
347
+ const state = loadState();
348
+ const feature = state.features[featureName];
349
+
350
+ const checkpoint = {
351
+ passed: true,
352
+ checkpointNum: feature.checkpoints.length + 1,
353
+ timestamp: new Date().toISOString(),
354
+ phase: feature.phase || 'setup',
355
+ results: [],
356
+ summary: {
357
+ note: note,
358
+ completedTasks: feature.tasks.completed
359
+ }
360
+ };
361
+
362
+ feature.checkpoints.push(checkpoint);
363
+ feature.updatedAt = new Date().toISOString();
364
+
365
+ saveState(state);
366
+ return checkpoint;
367
+ }
368
+
369
+ /**
370
+ * Add agent to feature
371
+ * @param {string} featureName - Feature name
372
+ * @param {string} agentId - Agent ID
373
+ * @returns {boolean} True if added, false if already exists
374
+ */
375
+ export async function addAgent(featureName, agentId) {
376
+ await ensureFeature(featureName);
377
+ const state = loadState(); // Load AFTER ensuring feature exists
378
+
379
+ if (!state.features[featureName].activeAgents.includes(agentId)) {
380
+ state.features[featureName].activeAgents.push(agentId);
381
+ state.features[featureName].updatedAt = new Date().toISOString();
382
+ saveState(state);
383
+ return true;
384
+ }
385
+
386
+ return false;
387
+ }
388
+
389
+ /**
390
+ * Remove agent from feature
391
+ * @param {string} featureName - Feature name
392
+ * @param {string} agentId - Agent ID
393
+ * @returns {boolean} True if removed, false if not found
394
+ */
395
+ export function removeAgent(featureName, agentId) {
396
+ const state = loadState();
397
+
398
+ if (!state.features[featureName]) {
399
+ throw new Error(`Feature '${featureName}' not found.`);
400
+ }
401
+
402
+ const index = state.features[featureName].activeAgents.indexOf(agentId);
403
+ if (index > -1) {
404
+ state.features[featureName].activeAgents.splice(index, 1);
405
+ state.features[featureName].updatedAt = new Date().toISOString();
406
+ saveState(state);
407
+ return true;
408
+ }
409
+
410
+ return false;
411
+ }
412
+
413
+ /**
414
+ * Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
415
+ * Supports both kebab-case and camelCase input formats for all output types
416
+ * @param {string} type - Output type (e.g., 'ui-design-system', 'schema-analysis', or 'uiDesignSystem')
417
+ * @returns {string} Normalized type in camelCase
418
+ */
419
+ function normalizeOutputType(type) {
420
+ // If string doesn't contain hyphens, assume it's already camelCase
421
+ if (!type.includes('-')) {
422
+ return type;
423
+ }
424
+
425
+ // Convert kebab-case to camelCase automatically
426
+ // Example: 'ui-design-system' → 'uiDesignSystem'
427
+ // Example: 'schema-analysis' → 'schemaAnalysis'
428
+ const camelCase = type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
429
+
430
+ return camelCase;
431
+ }
432
+
433
+ /**
434
+ * Calculate Levenshtein distance between two strings
435
+ * Used for fuzzy matching and typo detection
436
+ * @param {string} a - First string
437
+ * @param {string} b - Second string
438
+ * @returns {number} Edit distance
439
+ */
440
+ function levenshteinDistance(a, b) {
441
+ const matrix = [];
442
+
443
+ for (let i = 0; i <= b.length; i++) {
444
+ matrix[i] = [i];
445
+ }
446
+
447
+ for (let j = 0; j <= a.length; j++) {
448
+ matrix[0][j] = j;
449
+ }
450
+
451
+ for (let i = 1; i <= b.length; i++) {
452
+ for (let j = 1; j <= a.length; j++) {
453
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
454
+ matrix[i][j] = matrix[i - 1][j - 1];
455
+ } else {
456
+ matrix[i][j] = Math.min(
457
+ matrix[i - 1][j - 1] + 1, // substitution
458
+ matrix[i][j - 1] + 1, // insertion
459
+ matrix[i - 1][j] + 1 // deletion
460
+ );
461
+ }
462
+ }
463
+ }
464
+
465
+ return matrix[b.length][a.length];
466
+ }
467
+
468
+ /**
469
+ * Find closest matching output type for error suggestions
470
+ * @param {string} inputType - The invalid input type
471
+ * @param {Array<string>} validTypes - List of valid output types
472
+ * @returns {string|null} Closest match or null
473
+ */
474
+ function findClosestMatch(inputType, validTypes) {
475
+ let closestMatch = null;
476
+ let minDistance = Infinity;
477
+
478
+ const normalizedInput = inputType.toLowerCase();
479
+
480
+ for (const validType of validTypes) {
481
+ const distance = levenshteinDistance(normalizedInput, validType.toLowerCase());
482
+
483
+ // Consider it a match if distance is less than 3 (likely typo)
484
+ // or if one string contains the other (partial match)
485
+ if (distance < minDistance && distance <= 3) {
486
+ minDistance = distance;
487
+ closestMatch = validType;
488
+ }
489
+
490
+ // Also check for partial matches
491
+ if (validType.toLowerCase().includes(normalizedInput) ||
492
+ normalizedInput.includes(validType.toLowerCase())) {
493
+ if (distance < minDistance) {
494
+ minDistance = distance;
495
+ closestMatch = validType;
496
+ }
497
+ }
498
+ }
499
+
500
+ // Only suggest if similarity is reasonable
501
+ return minDistance <= 3 ? closestMatch : null;
502
+ }
503
+
504
+ /**
505
+ * Mark output as created
506
+ * @param {string} featureName - Feature name
507
+ * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
508
+ */
509
+ export async function markOutput(featureName, outputType) {
510
+ await ensureFeature(featureName);
511
+ const state = loadState();
512
+
513
+ const normalized = normalizeOutputType(outputType);
514
+
515
+ // Initialize outputs on demand (not pre-stored since v5.0.0)
516
+ if (!state.features[featureName].outputs) {
517
+ state.features[featureName].outputs = getAllOutputPaths(featureName);
518
+ }
519
+
520
+ // Define all valid output types (both camelCase and kebab-case alternatives)
521
+ const validTypes = [
522
+ 'proposal', 'spec', 'contracts', 'tasks', 'decisions', 'recap',
523
+ 'uiDesignSystem', 'ui-design-system',
524
+ 'uiMockups', 'ui-mockups',
525
+ 'uiComponents', 'ui-components',
526
+ 'uiFlows', 'ui-flows'
527
+ ];
528
+
529
+ if (!state.features[featureName].outputs[normalized]) {
530
+ // Try to find closest match for better error message
531
+ const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
532
+ const suggestion = findClosestMatch(outputType, validCamelCaseTypes);
533
+
534
+ let errorMsg = `Output type '${outputType}' not valid.`;
535
+
536
+ if (suggestion) {
537
+ errorMsg += `\n\nDid you mean '${suggestion}'?`;
538
+ // If the suggestion has a kebab-case alternative, show it too
539
+ const kebabAlternative = {
540
+ 'uiDesignSystem': 'ui-design-system',
541
+ 'uiMockups': 'ui-mockups',
542
+ 'uiComponents': 'ui-components',
543
+ 'uiFlows': 'ui-flows'
544
+ }[suggestion];
545
+ if (kebabAlternative) {
546
+ errorMsg += ` (also accepts '${kebabAlternative}')`;
547
+ }
548
+ }
549
+
550
+ errorMsg += `\n\nValid types:\n`;
551
+ errorMsg += ` - Standard: proposal, spec, contracts, tasks, decisions, recap\n`;
552
+ errorMsg += ` - UI/UX: uiDesignSystem, uiMockups, uiComponents, uiFlows\n`;
553
+ errorMsg += `\nNote: UI types also accept kebab-case (e.g., 'ui-design-system')`;
554
+
555
+ throw new Error(errorMsg);
556
+ }
557
+
558
+ state.features[featureName].outputs[normalized].created = true;
559
+ state.features[featureName].updatedAt = new Date().toISOString();
560
+
561
+ // Track the file change automatically
562
+ const outputPath = state.features[featureName].outputs[normalized].path;
563
+ if (outputPath) {
564
+ if (!Array.isArray(state.features[featureName].fileChanges)) {
565
+ state.features[featureName].fileChanges = [];
566
+ }
567
+ state.features[featureName].fileChanges.push({
568
+ path: outputPath,
569
+ action: 'created',
570
+ phase: state.features[featureName].phase,
571
+ timestamp: new Date().toISOString()
572
+ });
573
+ }
574
+
575
+ // If marking tasks output, try to sync task count from state tasks array
576
+ if (normalized === 'tasks') {
577
+ syncTasksCount(state.features[featureName]);
578
+ }
579
+
580
+ saveState(state);
581
+ }
582
+
583
+ /**
584
+ * Sync progress counters from taskList array into feature.progress
585
+ * @param {Object} feature - Feature object
586
+ */
587
+ function syncTasksCount(feature) {
588
+ const list = feature.taskList;
589
+ if (!Array.isArray(list) || list.length === 0) return;
590
+ const completed = list.filter(t => t.status === 'completed').length;
591
+ feature.progress = {
592
+ total: list.length,
593
+ completed,
594
+ inProgress: list.filter(t => t.status === 'in_progress').length,
595
+ pending: list.filter(t => t.status === 'pending').length,
596
+ percentage: Math.round((completed / list.length) * 100)
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Track a file change for a feature
602
+ * @param {string} featureName - Feature name
603
+ * @param {string} filePath - Path of the changed file
604
+ * @param {string} action - 'created' | 'modified' | 'deleted'
605
+ * @param {string} phase - Phase during which the change occurred
606
+ */
607
+ export async function trackFileChange(featureName, filePath, action, phase) {
608
+ await ensureFeature(featureName);
609
+ const state = loadState();
610
+
611
+ if (!Array.isArray(state.features[featureName].fileChanges)) {
612
+ state.features[featureName].fileChanges = [];
613
+ }
614
+
615
+ state.features[featureName].fileChanges.push({
616
+ path: filePath,
617
+ action,
618
+ phase: phase || state.features[featureName].phase || 'setup',
619
+ timestamp: new Date().toISOString()
620
+ });
621
+
622
+ state.features[featureName].updatedAt = new Date().toISOString();
623
+ saveState(state);
624
+ }
625
+
626
+ /**
627
+ * Get file changes for a feature, optionally grouped by phase
628
+ * @param {string} featureName - Feature name
629
+ * @param {Object} options - Options
630
+ * @param {boolean} options.groupByPhase - Group changes by phase
631
+ * @returns {Array|Object} File changes
632
+ */
633
+ export function getFileChanges(featureName, options = {}) {
634
+ const state = loadState();
635
+ const feature = state.features[featureName];
636
+
637
+ if (!feature) {
638
+ throw new Error(`Feature '${featureName}' not found.`);
639
+ }
640
+
641
+ const changes = feature.fileChanges || [];
642
+
643
+ if (options.groupByPhase) {
644
+ const grouped = {};
645
+ for (const change of changes) {
646
+ const phase = change.phase || 'unknown';
647
+ if (!grouped[phase]) grouped[phase] = [];
648
+ grouped[phase].push(change);
649
+ }
650
+ return grouped;
651
+ }
652
+
653
+ return changes;
654
+ }
655
+
656
+ /**
657
+ * Skip a phase and record it
658
+ * @param {string} featureName - Feature name
659
+ * @param {string} phase - Phase to skip
660
+ * @param {string} reason - Reason for skipping
661
+ */
662
+ export async function skipPhase(featureName, phase, reason = '') {
663
+ await ensureFeature(featureName);
664
+ const state = loadState();
665
+
666
+ if (!state.features[featureName].skippedPhases) {
667
+ state.features[featureName].skippedPhases = [];
668
+ }
669
+
670
+ // Don't add duplicates
671
+ const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
672
+ if (!existing) {
673
+ state.features[featureName].skippedPhases.push({
674
+ phase,
675
+ reason,
676
+ timestamp: new Date().toISOString()
677
+ });
678
+ }
679
+
680
+ state.features[featureName].updatedAt = new Date().toISOString();
681
+ saveState(state);
682
+ }
683
+
684
+ /**
685
+ * Get skipped phases for a feature
686
+ * @param {string} featureName - Feature name
687
+ * @returns {Array} Array of skipped phases
688
+ */
689
+ export function getSkippedPhases(featureName) {
690
+ const state = loadState();
691
+ if (!state.features[featureName]) {
692
+ return [];
693
+ }
694
+ return state.features[featureName].skippedPhases || [];
695
+ }
696
+
697
+ /**
698
+ * List all features
699
+ * @returns {Array} Array of [featureName, featureObject] tuples
700
+ */
701
+ export function listFeatures() {
702
+ const state = loadState();
703
+ return Object.entries(state.features);
704
+ }
705
+
706
+ /**
707
+ * Get project summary
708
+ * @returns {Object} Summary with metadata
709
+ */
710
+ export function getSummary() {
711
+ const state = loadState();
712
+ return {
713
+ project: state.project,
714
+ metadata: state.metadata,
715
+ featuresCount: Object.keys(state.features).length
716
+ };
717
+ }
718
+
719
+ // ============================================================================
720
+ // Approval Gates Operations
721
+ // ============================================================================
722
+
723
+ /**
724
+ * Set approval gate status
725
+ * @param {string} featureName - Feature name
726
+ * @param {string} gate - Gate name (proposal, uiux, design, tasks)
727
+ * @param {boolean} approved - Approval status
728
+ * @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
729
+ */
730
+ export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
731
+ const state = loadState();
732
+ const feature = await ensureFeature(featureName);
733
+
734
+ if (!feature.approvalGates) {
735
+ feature.approvalGates = {
736
+ proposal: { approved: false, timestamp: null, approvedBy: null },
737
+ uiux: { approved: false, timestamp: null, approvedBy: null },
738
+ design: { approved: false, timestamp: null, approvedBy: null },
739
+ tasks: { approved: false, timestamp: null, approvedBy: null }
740
+ };
741
+ }
742
+
743
+ feature.approvalGates[gate] = {
744
+ approved,
745
+ timestamp: metadata.approvedAt || metadata.rejectedAt || new Date().toISOString(),
746
+ approvedBy: metadata.approvedBy || metadata.rejectedBy || null,
747
+ ...metadata
748
+ };
749
+
750
+ state.features[featureName] = feature;
751
+ saveState(state);
752
+ }
753
+
754
+ /**
755
+ * Get approval gate status
756
+ * @param {string} featureName - Feature name
757
+ * @param {string} gate - Gate name
758
+ * @returns {Object|null} Gate object or null
759
+ */
760
+ export function getApprovalGate(featureName, gate) {
761
+ const state = loadState();
762
+ const feature = state.features[featureName];
763
+
764
+ if (!feature || !feature.approvalGates) {
765
+ return null;
766
+ }
767
+
768
+ return feature.approvalGates[gate] || null;
769
+ }
770
+
771
+ /**
772
+ * Check if feature is pending approval
773
+ * @param {string} featureName - Feature name
774
+ * @returns {boolean} True if any gate is pending approval
775
+ */
776
+ export function isPendingApproval(featureName) {
777
+ const state = loadState();
778
+ const feature = state.features[featureName];
779
+
780
+ if (!feature || !feature.approvalGates) {
781
+ return false;
782
+ }
783
+
784
+ const currentPhase = feature.phase || 'setup';
785
+
786
+ // Check if current phase has an approval gate
787
+ const phaseGateMap = {
788
+ 'design': 'design',
789
+ 'tasks': 'tasks',
790
+ 'uiux': 'uiux'
791
+ };
792
+
793
+ const relevantGate = phaseGateMap[currentPhase];
794
+ if (!relevantGate) {
795
+ return false;
796
+ }
797
+
798
+ const gate = feature.approvalGates[relevantGate];
799
+ return gate && !gate.approved;
800
+ }
801
+
802
+ /**
803
+ * Get approval history for a feature
804
+ * @param {string} featureName - Feature name
805
+ * @returns {Array} Array of approval events
806
+ */
807
+ export function getApprovalHistory(featureName) {
808
+ const state = loadState();
809
+ const feature = state.features[featureName];
810
+
811
+ if (!feature || !feature.approvalGates) {
812
+ return [];
813
+ }
814
+
815
+ const history = [];
816
+
817
+ Object.entries(feature.approvalGates).forEach(([gate, data]) => {
818
+ if (data.timestamp) {
819
+ history.push({
820
+ gate,
821
+ approved: data.approved,
822
+ timestamp: data.timestamp,
823
+ approvedBy: data.approvedBy || data.rejectedBy,
824
+ reason: data.reason
825
+ });
826
+ }
827
+ });
828
+
829
+ // Sort by timestamp
830
+ return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
831
+ }