@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,56 +1,29 @@
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
1
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
9
2
  import { join, dirname } from 'path';
10
3
  import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
4
+ import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
11
5
 
12
6
  const STATE_FILE_NAME = '.morph/state.json';
13
7
  const FEATURES_DIR_NAME = '.morph/features';
14
8
  const FEATURE_FILE_NAME = 'feature.json';
15
- /**
16
- * The one spelling of the committed project config's location, relative to a
17
- * project root. Exported because three modules used to write it out by hand
18
- * (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
19
- * src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
20
- * the writer and the reader "can never disagree about which file is meant",
21
- * a guarantee three copies of a string cannot give. Now they can't.
22
- */
23
9
  export const CONFIG_FILE_NAME = '.morph/config/config.json';
24
10
 
25
- // The on-disk state version. NOTE: this tracks the *feature-object* schema, which
26
- // is unchanged by the per-feature storage split — features are now persisted to
27
- // `.morph/features/{name}/feature.json` (authoritative, committed) with the root
28
- // `.morph/state.json` acting as a thin, derivable, gitignored index. Migration to
29
- // the split layout is detected by `feature.json` presence, not by this number.
11
+ // Versions the feature-object schema, not the storage layout: the per-feature
12
+ // split is detected by the presence of `feature.json`.
30
13
  const STATE_VERSION = '5.0.0';
31
14
 
32
- // ============================================================================
33
- // Core Functions
34
- // ============================================================================
35
-
36
- /**
37
- * Get the state file path (looks in current working directory)
38
- */
39
15
  export function getStatePath() {
40
16
  return join(process.cwd(), STATE_FILE_NAME);
41
17
  }
42
18
 
43
- /** Absolute path to the per-feature folder root (.morph/features). */
44
19
  function getFeaturesDir() {
45
20
  return join(process.cwd(), FEATURES_DIR_NAME);
46
21
  }
47
22
 
48
- /** Absolute path to a feature's authoritative feature.json. */
49
23
  function getFeatureJsonPath(featureName) {
50
24
  return join(getFeaturesDir(), featureName, FEATURE_FILE_NAME);
51
25
  }
52
26
 
53
- /** List feature names that have a folder on disk under .morph/features. */
54
27
  function listFeatureNamesOnDisk() {
55
28
  const dir = getFeaturesDir();
56
29
  if (!existsSync(dir)) return [];
@@ -63,7 +36,6 @@ function listFeatureNamesOnDisk() {
63
36
  }
64
37
  }
65
38
 
66
- /** Read a feature's feature.json, or null if missing/corrupt. */
67
39
  function readFeatureJson(featureName) {
68
40
  const path = getFeatureJsonPath(featureName);
69
41
  if (!existsSync(path)) return null;
@@ -74,7 +46,6 @@ function readFeatureJson(featureName) {
74
46
  }
75
47
  }
76
48
 
77
- /** Read project identity from the committed .morph/config/config.json, if present. */
78
49
  function readProjectFromConfig() {
79
50
  const path = join(process.cwd(), CONFIG_FILE_NAME);
80
51
  if (!existsSync(path)) return null;
@@ -91,10 +62,7 @@ function readProjectFromConfig() {
91
62
  }
92
63
  }
93
64
 
94
- /**
95
- * Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
96
- * for the brief Windows lock race. Creates the parent directory if needed.
97
- */
65
+ // Per-process temp file then rename; the EPERM retry covers the brief Windows lock race.
98
66
  export function atomicWriteJson(targetPath, data) {
99
67
  const dir = dirname(targetPath);
100
68
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -111,41 +79,25 @@ export function atomicWriteJson(targetPath, data) {
111
79
  if (err.code === 'EPERM' && attempt < MAX_RETRIES) {
112
80
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
113
81
  } else {
114
- try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
82
+ try { unlinkSync(tmpPath); } catch { }
115
83
  throw err;
116
84
  }
117
85
  }
118
86
  }
119
87
  }
120
88
 
121
- /**
122
- * Write a feature's authoritative feature.json (atomic).
123
- *
124
- * Skips the write entirely when the on-disk content is already byte-identical to
125
- * what we'd write. `saveState` hydrates and re-persists EVERY feature, so a CLI
126
- * mutation on feature A would otherwise rewrite sibling B's file from A's loaded
127
- * copy. Skipping unchanged siblings means A never touches B's authoritative file
128
- * in the common case — shrinking the cross-feature clobber window to nothing
129
- * unless B genuinely changed under A.
130
- *
131
- * A formatting mismatch is NOT free, though: the guard compares bytes, so a
132
- * sibling committed with a trailing newline fails it and gets rewritten. The
133
- * content is equivalent, but git sees ` M` on a file this caller never meant to
134
- * touch — enough to fail a whole-tree clean check (see the finish teardown's clearRootWorktreeRegistration).
135
- * And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
136
- * write to a sibling would still be clobbered by the sweep.
137
- */
89
+ // saveState re-persists every feature; skipping byte-identical content keeps a
90
+ // mutation on one feature from rewriting (and clobbering) its siblings' files.
138
91
  function writeFeatureJson(featureName, feature) {
139
92
  const path = getFeatureJsonPath(featureName);
140
93
  try {
141
94
  if (existsSync(path) && readFileSync(path, 'utf8') === JSON.stringify(feature, null, 2)) {
142
- return; // unchanged — don't rewrite (and don't clobber a concurrent sibling write)
95
+ return;
143
96
  }
144
- } catch { /* fall through to a normal write */ }
97
+ } catch { }
145
98
  atomicWriteJson(path, feature);
146
99
  }
147
100
 
148
- /** A fresh, empty state scaffold (used when no root index exists yet). */
149
101
  function scaffoldState() {
150
102
  return {
151
103
  version: STATE_VERSION,
@@ -155,67 +107,16 @@ function scaffoldState() {
155
107
  metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0 },
156
108
  };
157
109
  }
158
- /**
159
- * Derive current phase from filesystem.
160
- *
161
- * Walks the phase map in descending order and returns the first phase whose
162
- * CANONICAL ARTIFACT exists. An empty folder is not enough — `mkdir 2-plan/`
163
- * does not count as "in plan phase" until `spec.md` (the first canonical
164
- * plan-phase output) lands. If no canonical artifact is found anywhere,
165
- * returns `'proposal'` (the starting phase).
166
- *
167
- * This avoids the chicken-and-egg problem where the agent pre-creates all
168
- * phase folders, the derived phase jumps to the highest one, and
169
- * `enforce-phase-writes` then blocks the agent from writing back to the
170
- * prior phase folder.
171
- *
172
- * @param {string} featurePath - Absolute path to .morph/features/{feature}/
173
- * @returns {string} Phase name: 'review' | 'implement' | 'plan' | 'uiux' | 'proposal'
174
- */
175
- const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
176
-
177
- /** Phase implied purely by approved gates (mirrors hook-side gateImpliedPhase). */
178
- function gateImpliedPhaseFromGates(gates) {
179
- const ok = (g) => gates?.[g]?.approved === true;
180
- if (ok('review')) return 'review';
181
- if (ok('plan')) return 'implement';
182
- if (ok('uiux')) return 'plan';
183
- return 'proposal';
184
- }
185
-
110
+ // Delegates to the hook-side rule so the CLI and the hooks never disagree on a phase.
186
111
  export function derivePhase(featurePath) {
187
- const phaseMap = [
188
- ['4-review', 'review', 'review-report.md'],
189
- ['3-implement', 'implement', 'recap.md'],
190
- ['2-plan', 'plan', 'spec.md'],
191
- ['1-design', 'uiux', 'design-system.md'],
192
- ['0-proposal', 'proposal', 'proposal.md'],
193
- ];
194
- let artifactPhase = 'proposal';
195
- for (const [folder, phase, canonical] of phaseMap) {
196
- if (existsSync(join(featurePath, folder, canonical))) { artifactPhase = phase; break; }
197
- }
198
-
199
- // Gate-aware (same rule as the hook-side derivePhaseForFeature): an approved
200
- // gate advances the phase before the next phase's canonical artifact exists,
201
- // so the agent can actually write that first artifact.
202
- let gates = null;
112
+ let feature = null;
203
113
  try {
204
114
  const fjPath = join(featurePath, 'feature.json');
205
- if (existsSync(fjPath)) gates = JSON.parse(readFileSync(fjPath, 'utf8'))?.approvalGates;
206
- } catch { gates = null; }
207
- const implied = gateImpliedPhaseFromGates(gates);
208
- return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
115
+ if (existsSync(fjPath)) feature = JSON.parse(readFileSync(fjPath, 'utf8'));
116
+ } catch { feature = null; }
117
+ return derivePhaseFrom(feature, diskArtifactProbe(featurePath));
209
118
  }
210
119
 
211
- /**
212
- * Derive output existence from filesystem — checks if each output file exists at its expected path.
213
- * Returns an object matching the old outputs shape for backwards-compatible display.
214
- *
215
- * @param {string} featureName - Feature name
216
- * @param {string} [baseDir] - Project base dir (defaults to cwd)
217
- * @returns {Object} Map of outputType -> { created: boolean, path: string }
218
- */
219
120
  export function deriveOutputs(featureName, baseDir = process.cwd()) {
220
121
  const outputPaths = getAllOutputPaths(featureName);
221
122
  const result = {};
@@ -226,25 +127,16 @@ export function deriveOutputs(featureName, baseDir = process.cwd()) {
226
127
  return result;
227
128
  }
228
129
 
229
- /**
230
- * Check if state file exists
231
- */
232
130
  export function stateExists() {
233
131
  return existsSync(getStatePath());
234
132
  }
235
133
 
236
- /**
237
- * Load state from disk
238
- * @param {boolean} throwOnError - If false, returns null instead of throwing
239
- * @returns {Object|null} State object or null
240
- */
241
134
  export function loadState(throwOnError = true) {
242
135
  const statePath = getStatePath();
243
136
  const rootExists = existsSync(statePath);
244
137
  const featureNames = listFeatureNamesOnDisk();
245
138
  const featuresExist = featureNames.some((name) => existsSync(getFeatureJsonPath(name)));
246
139
 
247
- // "No state" only when neither the root index nor any per-feature file exists.
248
140
  if (!rootExists && !featuresExist) {
249
141
  if (throwOnError) {
250
142
  throw new Error(`State file not found: ${statePath}\nRun 'morph-spec init' to initialize MORPH in this project.`);
@@ -269,19 +161,12 @@ export function loadState(throwOnError = true) {
269
161
  if (fromConfig) state.project = { ...(state.project || {}), ...fromConfig };
270
162
  }
271
163
 
272
- // Snapshot the on-disk version BEFORE the migration blocks below bump it. The
273
- // reconciliation loop uses it to tell a legacy monolith (a pre-v5 layout that
274
- // predates the split — materialize) from a current-version index entry whose
275
- // folder is gone (a deleted feature — prune). A missing root index is treated
276
- // as "no migration" (null): the loop is empty in that case (features hydrate
277
- // from disk), so the value only matters when the root index exists.
164
+ // Snapshot before the migrations below bump it: the reconcile loop tells a
165
+ // legacy monolith (materialize) from a deleted feature (prune) by this value.
278
166
  const fileVersion = rootExists ? state.version : null;
279
167
 
280
- // A missing root index means we must (re)generate it from the per-feature
281
- // files we're about to hydrate — this is the self-heal path after a wipe.
282
168
  let mutated = !rootExists;
283
169
 
284
- // Migrate v3.0.0 → v4.0.0 (new directory layout) — operates on root features.
285
170
  if (state.version === '3.0.0') {
286
171
  state.version = '4.0.0';
287
172
  for (const [name, feature] of Object.entries(state.features)) {
@@ -294,8 +179,6 @@ export function loadState(throwOnError = true) {
294
179
  mutated = true;
295
180
  }
296
181
 
297
- // Migrate v4.x → v5.0.0: drop derived fields (now filesystem-derived) and
298
- // workflow selection (v5 has fixed phases).
299
182
  if (state.version && state.version.startsWith('4.')) {
300
183
  state.version = '5.0.0';
301
184
  for (const feature of Object.values(state.features)) {
@@ -307,57 +190,35 @@ export function loadState(throwOnError = true) {
307
190
  mutated = true;
308
191
  }
309
192
 
310
- // Reconcile index entries whose feature.json is missing. The discriminator is
311
- // the ON-DISK VERSION (`fileVersion`, snapshotted before the migrations bumped
312
- // it) — NOT whether any sibling folder survived. Sibling-existence is the wrong
313
- // signal: deleting EVERY folder at once (the reported incident) would leave no
314
- // survivor and resurrect them all. Version is unconditional:
315
- //
316
- // • A pre-v5 root (v3/v4) or an entry rich beyond the thin {status,updatedAt}
317
- // shape = a legacy monolithic state.json that predates the split. It is the
318
- // sole copy of its feature — materialize each to its authoritative
319
- // feature.json (split-on-load migration; idempotent thereafter).
320
- //
321
- // • A current-version (v5) thin entry with NO folder = the feature was
322
- // DELETED and this gitignored index entry is stale. Prune it; NEVER recreate
323
- // the folder — that turned an intentional delete into a zombie. This is safe
324
- // and exhaustive because saveState ALWAYS co-writes feature.json + a thin
325
- // index entry: a v5 thin entry with no feature.json can ONLY mean the folder
326
- // was removed. The prune sticks — saveState below derives the index from this
327
- // same in-memory map, and hydration only re-adds features whose folder exists.
193
+ // Index entry without feature.json: a pre-v5 or rich entry is the sole copy of a
194
+ // legacy monolith (materialize it); a v5 thin entry means the folder was deleted
195
+ // (prune it, never resurrect). Safe because saveState always co-writes both.
328
196
  const isRichEntry = (f) =>
329
197
  f && typeof f === 'object' && Object.keys(f).some((k) => k !== 'status' && k !== 'updatedAt');
330
198
  const wasVersionMigration = fileVersion !== null && fileVersion !== STATE_VERSION;
331
199
  for (const [name, feature] of Object.entries(state.features)) {
332
200
  if (existsSync(getFeatureJsonPath(name))) continue;
333
201
  if (wasVersionMigration || isRichEntry(feature)) {
334
- writeFeatureJson(name, feature); // legacy monolith — materialize, don't lose it
202
+ writeFeatureJson(name, feature);
335
203
  } else {
336
- delete state.features[name]; // v5 thin entry, no folder = deleted — never resurrect
204
+ delete state.features[name];
337
205
  console.error(`ℹ morph-spec: dropped stale index entry "${name}" (feature folder was deleted).`);
338
206
  }
339
207
  mutated = true;
340
208
  }
341
209
 
342
- // Hydrate from per-feature files (authoritative). Also picks up features that
343
- // exist on disk but are absent from a stale/deleted root index — so a wiped
344
- // state.json self-heals from the committed feature.json files.
345
210
  for (const name of featureNames) {
346
211
  const fj = readFeatureJson(name);
347
212
  if (fj) state.features[name] = fj;
348
213
  }
349
214
 
350
215
  if (mutated) {
351
- saveState(state); // re-persists feature.json files + a freshly thinned root index
216
+ saveState(state);
352
217
  }
353
218
 
354
219
  return state;
355
220
  }
356
221
 
357
- /**
358
- * Save state to disk
359
- * @param {Object} state - State object to save
360
- */
361
222
  export function saveState(state) {
362
223
  state.metadata = state.metadata || {};
363
224
  state.metadata.lastUpdated = new Date().toISOString();
@@ -365,32 +226,18 @@ export function saveState(state) {
365
226
 
366
227
  const features = state.features || {};
367
228
 
368
- // Aggregate metadata is derived from the authoritative feature set so the
369
- // thin root index never drifts from the per-feature files.
370
229
  state.metadata.totalFeatures = Object.keys(features).length;
371
230
  state.metadata.completedFeatures = Object.values(features)
372
231
  .filter((f) => f && f.status === 'completed').length;
373
232
 
374
- // 1) Persist each feature to its authoritative, committed feature.json — what
375
- // travels with the feature's own commit.
376
- //
377
- // NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
378
- // every sibling; writeFeatureJson's byte-identical guard makes that a no-op
379
- // only while a sibling's committed file matches our formatting exactly. A
380
- // sibling saved with a trailing newline (any editor, prettier) is rewritten
381
- // and shows up as ` M` — dirtying the tree on behalf of a feature the
382
- // caller never touched. Callers that need to change ONE feature's field
383
- // should write that feature.json directly instead of round-tripping state
384
- // through here (see the finish teardown's clearRootWorktreeRegistration).
233
+ // A cross-feature sweep: a sibling saved in another format (trailing newline) is
234
+ // rewritten and dirties the tree. To change one feature, write its feature.json directly.
385
235
  const thinFeatures = {};
386
236
  for (const [name, feature] of Object.entries(features)) {
387
237
  writeFeatureJson(name, feature);
388
238
  thinFeatures[name] = { status: feature.status, updatedAt: feature.updatedAt };
389
239
  }
390
240
 
391
- // 2) Persist the thin, derivable, gitignored root index (status + updatedAt
392
- // per feature is all that callers like getActiveFeature need from the root;
393
- // full feature data is hydrated from feature.json on load).
394
241
  atomicWriteJson(getStatePath(), {
395
242
  version: state.version,
396
243
  project: state.project,
@@ -400,20 +247,11 @@ export function saveState(state) {
400
247
  });
401
248
  }
402
249
 
403
- // ============================================================================
404
- // File Locking (BUG-002)
405
- // ============================================================================
406
-
407
250
  const LOCK_DIR_NAME = '.morph/state.json.lock';
408
251
  const LOCK_MAX_RETRIES = 5;
409
252
  const LOCK_BASE_MS = 100;
410
253
  const LOCK_STALE_MS = 30_000;
411
254
 
412
- /**
413
- * Wrap a read-modify-write cycle on state.json with a mkdir-based mutex.
414
- * @param {Function} fn - Async function to execute while holding the lock
415
- * @returns {Promise<*>} Return value of fn
416
- */
417
255
  export async function withStateLock(fn) {
418
256
  const lockPath = join(process.cwd(), LOCK_DIR_NAME);
419
257
 
@@ -444,14 +282,6 @@ export async function withStateLock(fn) {
444
282
  }
445
283
  }
446
284
 
447
- /**
448
- * Initialize new state file
449
- * @param {Object} options - Options
450
- * @param {boolean} options.force - Overwrite existing file
451
- * @param {string} options.projectName - Project name
452
- * @param {string} options.projectType - Project type (e.g., 'dotnet-nextjs')
453
- * @returns {Object} Initial state
454
- */
455
285
  export function initState(options = {}) {
456
286
  const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
457
287
 
@@ -481,27 +311,11 @@ export function initState(options = {}) {
481
311
  return initialState;
482
312
  }
483
313
 
484
- // ============================================================================
485
- // Feature Operations
486
- // ============================================================================
487
-
488
- /**
489
- * Get feature from state
490
- * @param {string} featureName - Feature name
491
- * @returns {Object|null} Feature object or null
492
- */
493
314
  export function getFeature(featureName) {
494
315
  const state = loadState();
495
316
  return state.features[featureName] || null;
496
317
  }
497
318
 
498
- /**
499
- * Create or get feature with default structure.
500
- * v5: every feature follows the fixed phase sequence
501
- * (proposal → uiux → plan → implement → review). No workflow selection.
502
- * @param {string} featureName - Feature name
503
- * @returns {Promise<Object>} Feature object
504
- */
505
319
  async function ensureFeature(featureName) {
506
320
  const state = loadState();
507
321
 
@@ -511,17 +325,10 @@ async function ensureFeature(featureName) {
511
325
  createdAt: new Date().toISOString(),
512
326
  updatedAt: new Date().toISOString(),
513
327
  approvalGates: {
514
- proposal: { approved: false, timestamp: null, approvedBy: null },
515
- uiux: { approved: false, timestamp: null, approvedBy: null },
516
328
  plan: { approved: false, timestamp: null, approvedBy: null },
329
+ uiux: { approved: false, timestamp: null, approvedBy: null },
517
330
  review: { approved: false, timestamp: null, approvedBy: null }
518
331
  },
519
- tasks: {
520
- total: 0,
521
- completed: 0,
522
- inProgress: 0,
523
- pending: 0
524
- },
525
332
  checkpoints: [],
526
333
  trustConfig: {},
527
334
  contextBundles: [],
@@ -535,28 +342,6 @@ async function ensureFeature(featureName) {
535
342
  return state.features[featureName];
536
343
  }
537
344
 
538
- /**
539
- * Explicitly create a feature with the canonical shape (documented in
540
- * framework/schemas/feature.schema.json). Idempotent: an existing feature is
541
- * returned untouched (including its `workType` — a later `--type` on a
542
- * repeat call never overwrites it; see `morph-spec retype` for that). This
543
- * is the server-side birth path behind `morph-spec create` — replacing the
544
- * implicit registration on the first Write into .morph/features/ (the
545
- * state-sync hook remains as fallback).
546
- *
547
- * Works even before any state exists (scaffolds the root index), so a
548
- * feature can be born as the very first morph action in a project.
549
- *
550
- * @param {string} featureName - Feature name (kebab-case), also the folder id
551
- * @param {{ status?: string, workType?: string, workTypeDetection?: object, name?: string, description?: string }} [options]
552
- * `name` defaults to `featureName` (the slug) — always stamped so a feature
553
- * is never nameless. `description` is written only when provided (the CLI
554
- * `create` enforces it; internal callers may omit it). `workType` defaults to
555
- * 'feature' (v8 routing — see src/core/work-types.js). `workTypeDetection` is
556
- * only written when provided (source/confidence/matched audit trail from the
557
- * `--type`/`--request` resolution in `create.js`).
558
- * @returns {Promise<{ feature: Object, created: boolean }>}
559
- */
560
345
  export async function createFeature(featureName, options = {}) {
561
346
  const { status = 'in_progress', workType, workTypeDetection, name, description } = options;
562
347
  const state = loadState(false) || scaffoldState();
@@ -575,18 +360,11 @@ export async function createFeature(featureName, options = {}) {
575
360
  createdAt: now,
576
361
  updatedAt: now,
577
362
  approvalGates: {
578
- proposal: { approved: false, timestamp: null, approvedBy: null },
579
- uiux: { approved: false, timestamp: null, approvedBy: null },
580
363
  plan: { approved: false, timestamp: null, approvedBy: null },
364
+ uiux: { approved: false, timestamp: null, approvedBy: null },
581
365
  review: { approved: false, timestamp: null, approvedBy: null }
582
366
  },
583
367
  gateDecisions: {},
584
- tasks: {
585
- total: 0,
586
- completed: 0,
587
- inProgress: 0,
588
- pending: 0
589
- },
590
368
  checkpoints: [],
591
369
  trustConfig: {},
592
370
  contextBundles: [],
@@ -602,12 +380,6 @@ export async function createFeature(featureName, options = {}) {
602
380
  return { feature: state.features[featureName], created: true };
603
381
  }
604
382
 
605
- /**
606
- * Update feature property (supports nested keys like "tasks.completed")
607
- * @param {string} featureName - Feature name
608
- * @param {string} key - Property key (supports dot notation)
609
- * @param {any} value - Value to set
610
- */
611
383
  export async function updateFeature(featureName, key, value) {
612
384
  await ensureFeature(featureName);
613
385
  const state = loadState(); // Load AFTER ensuring feature exists
@@ -629,11 +401,6 @@ export async function updateFeature(featureName, key, value) {
629
401
  saveState(state);
630
402
  }
631
403
 
632
- /**
633
- * Update multiple feature properties at once
634
- * @param {string} featureName - Feature name
635
- * @param {Object} updates - Object with key-value pairs to update
636
- */
637
404
  export async function updateFeatureMultiple(featureName, updates) {
638
405
  await ensureFeature(featureName);
639
406
  const state = loadState(); // Load AFTER ensuring feature exists
@@ -657,63 +424,16 @@ export async function updateFeatureMultiple(featureName, updates) {
657
424
  saveState(state);
658
425
  }
659
426
 
660
- /**
661
- * Add checkpoint to feature
662
- * @param {string} featureName - Feature name
663
- * @param {string} note - Checkpoint note
664
- * @returns {Object} Checkpoint object
665
- */
666
- export async function addCheckpoint(featureName, note) {
667
- await ensureFeature(featureName);
668
- const state = loadState();
669
- const feature = state.features[featureName];
670
-
671
- const checkpoint = {
672
- passed: true,
673
- checkpointNum: feature.checkpoints.length + 1,
674
- timestamp: new Date().toISOString(),
675
- phase: feature.phase || 'proposal',
676
- results: [],
677
- summary: {
678
- note: note,
679
- completedTasks: feature.tasks.completed
680
- }
681
- };
682
-
683
- feature.checkpoints.push(checkpoint);
684
- feature.updatedAt = new Date().toISOString();
685
-
686
- saveState(state);
687
- return checkpoint;
688
- }
689
-
690
- /**
691
- * Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
692
- * Supports both kebab-case and camelCase input formats for all output types
693
- * @param {string} type - Output type (e.g., 'ui-design-system', 'stack-scan', or 'uiDesignSystem')
694
- * @returns {string} Normalized type in camelCase
695
- */
696
427
  function normalizeOutputType(type) {
697
- // If string doesn't contain hyphens, assume it's already camelCase
698
428
  if (!type.includes('-')) {
699
429
  return type;
700
430
  }
701
431
 
702
- // Convert kebab-case to camelCase automatically
703
- // Example: 'ui-design-system' → 'uiDesignSystem'
704
- // Example: 'stack-scan' → 'stackScan'
705
432
  const camelCase = type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
706
433
 
707
434
  return camelCase;
708
435
  }
709
436
 
710
- /**
711
- * Calculate Levenshtein distance between two strings
712
- * Used for fuzzy matching and typo detection
713
- * @param {string} a - First string
714
- * @param {string} b - Second string
715
- * @returns {number} Edit distance
716
- */
717
437
  function levenshteinDistance(a, b) {
718
438
  const matrix = [];
719
439
 
@@ -731,9 +451,9 @@ function levenshteinDistance(a, b) {
731
451
  matrix[i][j] = matrix[i - 1][j - 1];
732
452
  } else {
733
453
  matrix[i][j] = Math.min(
734
- matrix[i - 1][j - 1] + 1, // substitution
735
- matrix[i][j - 1] + 1, // insertion
736
- matrix[i - 1][j] + 1 // deletion
454
+ matrix[i - 1][j - 1] + 1,
455
+ matrix[i][j - 1] + 1,
456
+ matrix[i - 1][j] + 1
737
457
  );
738
458
  }
739
459
  }
@@ -742,12 +462,6 @@ function levenshteinDistance(a, b) {
742
462
  return matrix[b.length][a.length];
743
463
  }
744
464
 
745
- /**
746
- * Find closest matching output type for error suggestions
747
- * @param {string} inputType - The invalid input type
748
- * @param {Array<string>} validTypes - List of valid output types
749
- * @returns {string|null} Closest match or null
750
- */
751
465
  function findClosestMatch(inputType, validTypes) {
752
466
  let closestMatch = null;
753
467
  let minDistance = Infinity;
@@ -757,14 +471,11 @@ function findClosestMatch(inputType, validTypes) {
757
471
  for (const validType of validTypes) {
758
472
  const distance = levenshteinDistance(normalizedInput, validType.toLowerCase());
759
473
 
760
- // Consider it a match if distance is less than 3 (likely typo)
761
- // or if one string contains the other (partial match)
762
474
  if (distance < minDistance && distance <= 3) {
763
475
  minDistance = distance;
764
476
  closestMatch = validType;
765
477
  }
766
478
 
767
- // Also check for partial matches
768
479
  if (validType.toLowerCase().includes(normalizedInput) ||
769
480
  normalizedInput.includes(validType.toLowerCase())) {
770
481
  if (distance < minDistance) {
@@ -774,28 +485,20 @@ function findClosestMatch(inputType, validTypes) {
774
485
  }
775
486
  }
776
487
 
777
- // Only suggest if similarity is reasonable
778
488
  return minDistance <= 3 ? closestMatch : null;
779
489
  }
780
490
 
781
- /**
782
- * Mark output as created
783
- * @param {string} featureName - Feature name
784
- * @param {string} outputType - Output type (proposal, spec, tasks, etc.)
785
- */
786
491
  export async function markOutput(featureName, outputType) {
787
492
  await ensureFeature(featureName);
788
493
  const state = loadState();
789
494
 
790
495
  const normalized = normalizeOutputType(outputType);
791
496
 
792
- // Initialize outputs on demand (not pre-stored)
793
497
  if (!state.features[featureName].outputs) {
794
498
  state.features[featureName].outputs = getAllOutputPaths(featureName);
795
499
  }
796
500
 
797
501
  if (!state.features[featureName].outputs[normalized]) {
798
- // Try to find closest match for better error message
799
502
  const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
800
503
  const suggestion = findClosestMatch(outputType, validCamelCaseTypes);
801
504
 
@@ -803,7 +506,6 @@ export async function markOutput(featureName, outputType) {
803
506
 
804
507
  if (suggestion) {
805
508
  errorMsg += `\n\nDid you mean '${suggestion}'?`;
806
- // If the suggestion has a kebab-case alternative, show it too
807
509
  const kebabAlternative = {
808
510
  'uiDesignSystem': 'ui-design-system',
809
511
  'uiMockups': 'ui-mockups',
@@ -825,7 +527,6 @@ export async function markOutput(featureName, outputType) {
825
527
  state.features[featureName].outputs[normalized].created = true;
826
528
  state.features[featureName].updatedAt = new Date().toISOString();
827
529
 
828
- // Track the file change automatically
829
530
  const outputPath = state.features[featureName].outputs[normalized].path;
830
531
  if (outputPath) {
831
532
  if (!Array.isArray(state.features[featureName].fileChanges)) {
@@ -842,13 +543,6 @@ export async function markOutput(featureName, outputType) {
842
543
  saveState(state);
843
544
  }
844
545
 
845
- /**
846
- * Track a file change for a feature
847
- * @param {string} featureName - Feature name
848
- * @param {string} filePath - Path of the changed file
849
- * @param {string} action - 'created' | 'modified' | 'deleted'
850
- * @param {string} phase - Phase during which the change occurred
851
- */
852
546
  export async function trackFileChange(featureName, filePath, action, phase) {
853
547
  await ensureFeature(featureName);
854
548
  const state = loadState();
@@ -860,7 +554,7 @@ export async function trackFileChange(featureName, filePath, action, phase) {
860
554
  state.features[featureName].fileChanges.push({
861
555
  path: filePath,
862
556
  action,
863
- phase: phase || state.features[featureName].phase || 'proposal',
557
+ phase: phase || state.features[featureName].phase || 'plan',
864
558
  timestamp: new Date().toISOString()
865
559
  });
866
560
 
@@ -868,13 +562,6 @@ export async function trackFileChange(featureName, filePath, action, phase) {
868
562
  saveState(state);
869
563
  }
870
564
 
871
- /**
872
- * Get file changes for a feature, optionally grouped by phase
873
- * @param {string} featureName - Feature name
874
- * @param {Object} options - Options
875
- * @param {boolean} options.groupByPhase - Group changes by phase
876
- * @returns {Array|Object} File changes
877
- */
878
565
  export function getFileChanges(featureName, options = {}) {
879
566
  const state = loadState();
880
567
  const feature = state.features[featureName];
@@ -898,12 +585,6 @@ export function getFileChanges(featureName, options = {}) {
898
585
  return changes;
899
586
  }
900
587
 
901
- /**
902
- * Skip a phase and record it
903
- * @param {string} featureName - Feature name
904
- * @param {string} phase - Phase to skip
905
- * @param {string} reason - Reason for skipping
906
- */
907
588
  export async function skipPhase(featureName, phase, reason = '') {
908
589
  await ensureFeature(featureName);
909
590
  const state = loadState();
@@ -912,7 +593,6 @@ export async function skipPhase(featureName, phase, reason = '') {
912
593
  state.features[featureName].skippedPhases = [];
913
594
  }
914
595
 
915
- // Don't add duplicates
916
596
  const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
917
597
  if (!existing) {
918
598
  state.features[featureName].skippedPhases.push({
@@ -926,11 +606,6 @@ export async function skipPhase(featureName, phase, reason = '') {
926
606
  saveState(state);
927
607
  }
928
608
 
929
- /**
930
- * Get skipped phases for a feature
931
- * @param {string} featureName - Feature name
932
- * @returns {Array} Array of skipped phases
933
- */
934
609
  export function getSkippedPhases(featureName) {
935
610
  const state = loadState();
936
611
  if (!state.features[featureName]) {
@@ -939,19 +614,11 @@ export function getSkippedPhases(featureName) {
939
614
  return state.features[featureName].skippedPhases || [];
940
615
  }
941
616
 
942
- /**
943
- * List all features
944
- * @returns {Array} Array of [featureName, featureObject] tuples
945
- */
946
617
  export function listFeatures() {
947
618
  const state = loadState();
948
619
  return Object.entries(state.features);
949
620
  }
950
621
 
951
- /**
952
- * Get project summary
953
- * @returns {Object} Summary with metadata
954
- */
955
622
  export function getSummary() {
956
623
  const state = loadState();
957
624
  return {
@@ -961,26 +628,14 @@ export function getSummary() {
961
628
  };
962
629
  }
963
630
 
964
- // ============================================================================
965
- // Approval Gates Operations
966
- // ============================================================================
967
-
968
- /**
969
- * Set approval gate status
970
- * @param {string} featureName - Feature name
971
- * @param {string} gate - Gate name (proposal, uiux, plan, review)
972
- * @param {boolean} approved - Approval status
973
- * @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
974
- */
975
631
  export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
976
632
  const state = loadState();
977
633
  const feature = await ensureFeature(featureName);
978
634
 
979
635
  if (!feature.approvalGates) {
980
636
  feature.approvalGates = {
981
- proposal: { approved: false, timestamp: null, approvedBy: null },
982
- uiux: { approved: false, timestamp: null, approvedBy: null },
983
637
  plan: { approved: false, timestamp: null, approvedBy: null },
638
+ uiux: { approved: false, timestamp: null, approvedBy: null },
984
639
  review: { approved: false, timestamp: null, approvedBy: null }
985
640
  };
986
641
  }
@@ -996,12 +651,6 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
996
651
  saveState(state);
997
652
  }
998
653
 
999
- /**
1000
- * Get approval gate status
1001
- * @param {string} featureName - Feature name
1002
- * @param {string} gate - Gate name
1003
- * @returns {Object|null} Gate object or null
1004
- */
1005
654
  export function getApprovalGate(featureName, gate) {
1006
655
  const state = loadState();
1007
656
  const feature = state.features[featureName];
@@ -1013,11 +662,6 @@ export function getApprovalGate(featureName, gate) {
1013
662
  return feature.approvalGates[gate] || null;
1014
663
  }
1015
664
 
1016
- /**
1017
- * Check if feature is pending approval
1018
- * @param {string} featureName - Feature name
1019
- * @returns {boolean} True if any gate is pending approval
1020
- */
1021
665
  export function isPendingApproval(featureName) {
1022
666
  const state = loadState();
1023
667
  const feature = state.features[featureName];
@@ -1026,9 +670,8 @@ export function isPendingApproval(featureName) {
1026
670
  return false;
1027
671
  }
1028
672
 
1029
- const currentPhase = feature.phase || 'proposal';
673
+ const currentPhase = feature.phase || 'plan';
1030
674
 
1031
- // Check if current phase has an approval gate
1032
675
  const phaseGateMap = {
1033
676
  'plan': 'plan',
1034
677
  'review': 'review',
@@ -1044,11 +687,6 @@ export function isPendingApproval(featureName) {
1044
687
  return gate && !gate.approved;
1045
688
  }
1046
689
 
1047
- /**
1048
- * Get approval history for a feature
1049
- * @param {string} featureName - Feature name
1050
- * @returns {Array} Array of approval events
1051
- */
1052
690
  export function getApprovalHistory(featureName) {
1053
691
  const state = loadState();
1054
692
  const feature = state.features[featureName];
@@ -1071,6 +709,5 @@ export function getApprovalHistory(featureName) {
1071
709
  }
1072
710
  });
1073
711
 
1074
- // Sort by timestamp
1075
712
  return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
1076
713
  }