@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,63 +1,16 @@
1
- /**
2
- * reparse-scan.js — exhaustive, fail-closed reparse-point (symlink/junction)
3
- * scan of a directory tree.
4
- *
5
- * This lives in `framework/hooks/shared/` rather than `src/` because BOTH sides
6
- * need it and the dependency direction in this repo only runs one way: `src/`
7
- * imports from `framework/hooks/shared/` (see `finish.js`'s `claim-registry`
8
- * and `e2e-check` imports), never the reverse — a hook shipped into a target
9
- * project has the package, not this repo's `src/` layout.
10
- *
11
- * Two callers, one question:
12
- * - `teardownWorktree` (src/commands/project/worktree.js) — may this worktree
13
- * be handed to `git worktree remove`?
14
- * - the `block-destructive-worktree-removal` PreToolUse hook — is the command
15
- * about to run going to traverse a link out of the worktree?
16
- *
17
- * WHY IT MUST BE EXHAUSTIVE — both `git worktree remove` and its `--force`
18
- * fallback FOLLOW reparse points out of the worktree and delete the PRIMARY
19
- * ROOT's real files. The plain removal is not the safe one: it refuses only
20
- * when it sees modified/untracked *tracked-scope* content, so a GITIGNORED
21
- * junction (`node_modules`, `.claude`) leaves `git status` empty, the removal
22
- * proceeds, exits 0 — and the target is gone.
23
- *
24
- * So the check cannot live in the `catch` of the plain removal (unreachable on
25
- * exactly the destructive path), and it cannot be a fixed list of the links
26
- * `setup` creates: a nested link inside a real `node_modules` (npm link, npm
27
- * workspaces) is invisible to any top-level inventory. We scan the whole tree
28
- * and NEVER recurse INTO a reparse point — descending one would walk the target
29
- * we are trying to protect (and could not terminate on a cycle).
30
- *
31
- * FAIL-CLOSED, deliberately: an empty result is NOT the same as a safe one. A
32
- * directory we could not read or an entry we could not lstat might BE (or
33
- * contain) a reparse point. Those answer `unprovable`, and every caller must
34
- * treat that exactly like a found link — decline. "I saw nothing" and "I proved
35
- * there is nothing" are different claims, and only the second may authorize a
36
- * removal. This is the one module in the shared/ directory that must NOT be
37
- * fail-open; the whole point is that uncertainty blocks.
38
- *
39
- * `lstat` reports a POSIX symlink and a Windows junction alike as a symbolic
40
- * link, so the scan behaves identically on both platforms — as does the
41
- * destruction it prevents.
42
- */
1
+ // `git worktree remove` (plain or --force) follows reparse points and deletes the
2
+ // primary root's files, nested links included. FAIL-CLOSED: an entry that cannot
3
+ // be read is `unprovable` and blocks exactly like a found link.
43
4
 
44
5
  import { readdirSync, lstatSync } from 'fs';
45
6
  import { join, relative } from 'path';
46
7
 
47
- /**
48
- * Every reparse point anywhere under `dir`, as relative paths.
49
- *
50
- * @param {string} dir
51
- * @param {string} [base] - root of the scan, for relative reporting
52
- * @param {{points: string[], unprovable: string[]}} [acc]
53
- * @returns {{points: string[], unprovable: string[]}} both empty = provably clean
54
- */
8
+ /** @returns {{points: string[], unprovable: string[]}} both empty = provably clean */
55
9
  export function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }) {
56
10
  let entries;
57
11
  try {
58
12
  entries = readdirSync(dir, { withFileTypes: true });
59
13
  } catch {
60
- // Unreadable (ACL, lock, EMFILE): we cannot prove what is in here.
61
14
  acc.unprovable.push((relative(base, dir) || '.').replace(/\\/g, '/'));
62
15
  return acc;
63
16
  }
@@ -71,28 +24,15 @@ export function findReparsePoints(dir, base = dir, acc = { points: [], unprovabl
71
24
  continue;
72
25
  }
73
26
  if (st.isSymbolicLink()) {
74
- // Forward slashes: every other path this command reports is normalized,
75
- // and these end up in JSON the caller may compare or display.
76
27
  acc.points.push(relative(base, p).replace(/\\/g, '/'));
77
- continue; // do NOT descend — that is the target, not the worktree
28
+ continue; // descending would walk the target, not the worktree
78
29
  }
79
30
  if (st.isDirectory() && entry.name !== '.git') findReparsePoints(p, base, acc);
80
31
  }
81
32
  return acc;
82
33
  }
83
34
 
84
- /**
85
- * The single question both callers actually ask: may this tree be handed to a
86
- * command that follows links?
87
- *
88
- * Collapses `points` and `unprovable` into one list precisely because callers
89
- * must not weigh them differently — `teardownWorktree` already concatenated
90
- * them, and a hook that treated `unprovable` as "probably fine" would reopen
91
- * the hole this module exists to close.
92
- *
93
- * @param {string} dir
94
- * @returns {{ safe: boolean, blockers: string[] }}
95
- */
35
+ // One list on purpose: callers must not weigh `unprovable` lighter than a link.
96
36
  export function scanForBlockers(dir) {
97
37
  const { points, unprovable } = findReparsePoints(dir);
98
38
  const blockers = [...points, ...unprovable];
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
5
+ export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
6
+
7
+ export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
8
+
9
+ export function checksPath(featureDir) {
10
+ return join(featureDir, '2-plan', 'checks.json');
11
+ }
12
+
13
+ export function sliceReportPath(featureDir, sliceId) {
14
+ return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
15
+ }
16
+
17
+ export function sliceStatusFromReport(text) {
18
+ if (text == null) return 'pendente';
19
+ const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
20
+ return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
21
+ }
22
+
23
+ /** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
24
+ export function sliceProgressFrom(checksText, readReport) {
25
+ let slices;
26
+ try {
27
+ slices = JSON.parse(checksText ?? '').slices;
28
+ } catch {
29
+ return null;
30
+ }
31
+ if (!Array.isArray(slices)) return null;
32
+ const out = slices
33
+ .filter((s) => s && typeof s.id === 'string')
34
+ .map((s) => ({
35
+ id: s.id,
36
+ title: s.title ?? '',
37
+ depends: Array.isArray(s.depends) ? s.depends : [],
38
+ covers: Array.isArray(s.covers) ? s.covers : [],
39
+ status: sliceStatusFromReport(readReport(s.id)),
40
+ }));
41
+ const count = (status) => out.filter((s) => s.status === status).length;
42
+ return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
43
+ }
44
+
45
+ function readOrNull(path) {
46
+ try {
47
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export function sliceProgress(featureDir) {
54
+ return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
55
+ }
56
+
57
+ export function readySlices(progress) {
58
+ if (!progress) return [];
59
+ const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
60
+ return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
61
+ }
62
+
63
+ // Loose on purpose: a false "complete" costs a read, a false "incomplete"
64
+ // re-dispatches finished work.
65
+ export const REPORT_MARKERS = [
66
+ { id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
67
+ ];
68
+
69
+ const MIN_PROSE_CHARS = 80;
70
+
71
+ const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
72
+
73
+ /** Every scenario in `covers` must be named in the report. */
74
+ export function inspectReport(path, { covers = [] } = {}) {
75
+ const missingAll = ['what', 'files', ...covers];
76
+ if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
77
+ let body = '';
78
+ try {
79
+ body = readFileSync(path, 'utf8');
80
+ } catch {
81
+ return { exists: true, complete: false, missing: missingAll, unreadable: true };
82
+ }
83
+
84
+ const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
85
+ missing.push(...covers.filter((id) => !scenarioRe(id).test(body)));
86
+ const prose = body
87
+ .split('\n')
88
+ .filter((l) => !/^\s*(#|[-*]\s|\*\*)/.test(l))
89
+ .join(' ')
90
+ .replace(/\s+/g, ' ')
91
+ .trim();
92
+ if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
93
+
94
+ const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
95
+ const partial = PARTIAL_MARKER.test(firstLine);
96
+ if (partial) missing.push('partial');
97
+
98
+ return { exists: true, complete: missing.length === 0, missing, partial };
99
+ }
@@ -1,34 +1,21 @@
1
- /**
2
- * Which files count as the project's hand-written source. One reading for the
3
- * `reuse` and `code-budget` validators and the `comment-guard` hook, so the
4
- * three never disagree about the same file.
5
- *
6
- * @module source-files
7
- */
1
+ // One reading of "hand-written project source" for the `reuse` and
2
+ // `code-budget` validators and the `comment-guard` hook.
8
3
 
9
4
  const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
10
5
 
11
- // `.morph/` is real: an archived feature's `contracts.cs` entered ProspectPRO's
12
- // graph as a second `RoulettePriority`.
6
+ // `.morph/` holds archived contracts that would duplicate real types.
13
7
  const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
14
8
 
15
9
  const DECLARATION_FILE = /\.d\.ts$/i;
16
10
 
17
11
  const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
18
12
 
19
- /**
20
- * @param {string} sourceFile
21
- * @returns {boolean} project code in a scanned extension, tests included
22
- */
13
+ /** Tests included. */
23
14
  export function isProjectSource(sourceFile) {
24
15
  const f = String(sourceFile || '').replace(/\\/g, '/');
25
16
  return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
26
17
  }
27
18
 
28
- /**
29
- * @param {string} sourceFile
30
- * @returns {boolean} generated C# nobody writes by hand
31
- */
32
19
  export function isGenerated(sourceFile) {
33
20
  return GENERATED.test(String(sourceFile || ''));
34
21
  }
@@ -0,0 +1,23 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const NEEDS_CMD_SHIM = /^(npm|npx|pnpm|yarn)$/;
5
+
6
+ /**
7
+ * On win32 `npx`/`npm`/… are `.cmd` shims: ENOENT without a shell, EINVAL as `.cmd`
8
+ * with `shell: false`. Playwright runs from its own JS entry, so it needs no shell.
9
+ */
10
+ export function resolveSpawn(file, args = [], {
11
+ cwd = process.cwd(),
12
+ platform = process.platform,
13
+ exists = existsSync,
14
+ execPath = process.execPath,
15
+ } = {}) {
16
+ if (platform !== 'win32' || !NEEDS_CMD_SHIM.test(file)) return { file, args, shell: false };
17
+
18
+ if (file === 'npx' && args[0] === 'playwright') {
19
+ const cli = join(cwd, 'node_modules', '@playwright', 'test', 'cli.js');
20
+ if (exists(cli)) return { file: execPath, args: [cli, ...args.slice(1)], shell: false };
21
+ }
22
+ return { file: `${file}.cmd`, args, shell: true };
23
+ }
@@ -1,11 +1,4 @@
1
- /**
2
- * Shared read-only state accessor for Claude Code hooks.
3
- *
4
- * Wraps state.json reads with fail-open semantics:
5
- * - Returns null instead of throwing on missing/corrupt state
6
- * - Never writes to state.json (read-only)
7
- * - Uses synchronous reads for <100ms performance
8
- */
1
+ // Fail-open: missing or corrupt state reads as null, never throws.
9
2
 
10
3
  import { readFileSync, existsSync, readdirSync } from 'fs';
11
4
  import { join } from 'path';
@@ -13,80 +6,15 @@ import { join } from 'path';
13
6
  const STATE_FILE = '.morph/state.json';
14
7
  const FEATURES_DIR = '.morph/features';
15
8
 
16
- /**
17
- * Module-level memoization of `loadState()`, keyed by `projectPath`.
18
- *
19
- * `loadState()` does a `readdirSync` of `.morph/features` plus a
20
- * `readFileSync` + `JSON.parse` of EVERY `feature.json` under it, on every
21
- * call. A single hook invocation calls it 1-3x (directly, or via
22
- * `getActiveFeature`/`derivePhaseForFeature`/`isGateApproved`/etc.) — re-doing
23
- * that full hydration each time is pure waste when nothing on disk changed
24
- * between those calls.
25
- *
26
- * SAFE because every hook process is short-lived and single-invocation: one
27
- * `node state-reader.js`-importing process handles exactly one tool call (or
28
- * one CLI command) and exits. There is no risk of the cache going stale
29
- * ACROSS invocations, because each invocation starts with an empty Map.
30
- * Within a SINGLE invocation, staleness would only matter if that same
31
- * process both reads and then writes a feature's `feature.json` and
32
- * expects a LATER read to see the write — verified (grep across every
33
- * consumer, 2026-08-11) that the one hook that writes feature.json
34
- * (`state-sync.js`) never re-reads through `loadState()` after writing in the
35
- * same invocation; its `AskUserQuestion` handler reads via
36
- * `readFeatureFile()` (a different, uncached module) as the PRIMARY path and
37
- * only falls back to `loadState()` for a feature that has NEVER had a
38
- * feature.json written — which, by construction, can't have just been
39
- * written to in this same invocation either.
40
- *
41
- * A directory-mtime-based invalidation was considered instead (so a
42
- * LONG-LIVED consumer could reuse this module safely too) but rejected on two
43
- * grounds: (1) verified — grep for every importer of this file — that none is
44
- * long-lived (they're all hooks, one process per tool call, or CLI commands,
45
- * one process per invocation; the dashboard is a separate Vite/React app and
46
- * the statusline is Python, neither imports this module); and (2) even if one
47
- * existed, `.morph/features`'s own mtime only changes when feature
48
- * DIRECTORIES are added/removed, not when an EXISTING feature.json's content
49
- * is overwritten in place — so mtime-of-the-dir can't actually detect the
50
- * mutation that matters here, and getting that right would mean stat'ing
51
- * every feature.json individually, which re-adds the very cost this cache
52
- * exists to remove. If a long-lived consumer is ever added, it should
53
- * re-derive state per request rather than lean on this cache.
54
- *
55
- * CAVEAT for any future consumer: the object returned by `loadState()` is the
56
- * SAME object on every cache hit, not a fresh copy — a caller that mutates it
57
- * (as `state-sync.js`'s `handleWriteEdit` does, in-place, before persisting)
58
- * is mutating the cached entry too. That's harmless today because nothing
59
- * currently reads the cache again after such a mutation within one process,
60
- * but treat the return value as read-only unless you've re-verified that.
61
- *
62
- * `clearStateCache()` is exported for tests that write/rewrite feature.json
63
- * and then call a `loadState()`-backed function again WITHIN THE SAME
64
- * process and need to see the fresh content — call it at the point of
65
- * mutation.
66
- */
9
+ // Memoized per process: each hook/CLI process handles one call. The returned
10
+ // object is shared, so treat it as read-only; writers call `clearStateCache()`.
67
11
  const stateCache = new Map();
68
12
 
69
- /** Drop all cached state — see `stateCache` above for when this is needed. */
70
13
  export function clearStateCache() {
71
14
  stateCache.clear();
72
15
  }
73
16
 
74
- /**
75
- * Load state from the project root, hydrating each feature from its
76
- * authoritative per-feature `feature.json`.
77
- *
78
- * The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
79
- * per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
80
- * (committed). This reader merges them: it reads the thin index for project /
81
- * metadata and overlays each feature.json so callers see the full feature object.
82
- * A single corrupt feature.json is skipped, not fatal.
83
- *
84
- * Fail-open: returns null on any unexpected error or when there is no state at
85
- * all (no index and no feature files). Memoized per `projectPath` for the
86
- * lifetime of this process — see `stateCache` above.
87
- * @param {string} [projectPath] - Project root (defaults to cwd)
88
- * @returns {Object|null} State object or null
89
- */
17
+ /** The thin `state.json` index overlaid with each authoritative `feature.json`; null when neither exists. */
90
18
  export function loadState(projectPath) {
91
19
  const base = projectPath || process.cwd();
92
20
  if (stateCache.has(base)) return stateCache.get(base);
@@ -119,7 +47,7 @@ export function loadState(projectPath) {
119
47
  state.features = state.features || {};
120
48
  state.features[name] = fj; // feature.json is authoritative
121
49
  } catch {
122
- // skip a single corrupt feature.json — don't blind every hook
50
+ // one corrupt feature.json must not blind every hook
123
51
  }
124
52
  }
125
53
  }
@@ -134,17 +62,8 @@ export function loadState(projectPath) {
134
62
  return state;
135
63
  }
136
64
 
137
- /**
138
- * Check if there is state to read: either the thin `.morph/state.json` index
139
- * exists, or `.morph/features/` (committed, gitignore-exempt) already has at
140
- * least one feature with a `feature.json`. The latter case covers a freshly
141
- * checked-out worktree where state.json (gitignored) hasn't been materialized
142
- * yet, but `loadState()` can already self-heal from feature.json — so hooks
143
- * gating on `stateExists()` should be "on" there too, not dark.
144
- * Fail-open: any unexpected error (e.g. permission denied) returns false.
145
- * @param {string} [projectPath]
146
- * @returns {boolean}
147
- */
65
+ // A fresh worktree has `feature.json` but no gitignored `state.json` yet; hooks
66
+ // must stay on there.
148
67
  export function stateExists(projectPath) {
149
68
  try {
150
69
  const base = projectPath || process.cwd();
@@ -163,11 +82,6 @@ export function stateExists(projectPath) {
163
82
  }
164
83
  }
165
84
 
166
- /**
167
- * Get the active feature (most recently updated in_progress feature).
168
- * @param {string} [projectPath]
169
- * @returns {{ name: string, feature: Object }|null}
170
- */
171
85
  export function getActiveFeature(projectPath) {
172
86
  const state = loadState(projectPath);
173
87
  if (!state?.features) return null;
@@ -188,12 +102,6 @@ export function getActiveFeature(projectPath) {
188
102
  return active;
189
103
  }
190
104
 
191
- /**
192
- * Get the most recently updated feature (any status).
193
- * Useful as a fallback when getActiveFeature returns null (all features done).
194
- * @param {string} [projectPath]
195
- * @returns {{ name: string, feature: Object }|null}
196
- */
197
105
  export function getMostRecentFeature(projectPath) {
198
106
  const state = loadState(projectPath);
199
107
  if (!state?.features) return null;
@@ -212,155 +120,94 @@ export function getMostRecentFeature(projectPath) {
212
120
  return result;
213
121
  }
214
122
 
215
- /**
216
- * Get a specific feature by name.
217
- * @param {string} featureName
218
- * @param {string} [projectPath]
219
- * @returns {Object|null}
220
- */
221
123
  export function getFeature(featureName, projectPath) {
222
124
  const state = loadState(projectPath);
223
125
  return state?.features?.[featureName] || null;
224
126
  }
225
127
 
226
- /**
227
- * Derive phase from filesystem (phase deleted from state).
228
- *
229
- * Walks the phase map in descending order and returns the first phase whose
230
- * CANONICAL ARTIFACT exists. An empty folder is not enough — this avoids the
231
- * chicken-and-egg problem where `mkdir 2-plan/` (or any pre-created sibling)
232
- * advances the phase before plan content is written, then
233
- * `enforce-phase-writes` blocks writes back to `0-proposal/`.
234
- *
235
- * Canonical artifacts (one per phase) match MORPH.md §3:
236
- * - 4-review → review-report.md
237
- * - 3-implement → recap.md
238
- * - 2-plan → spec.md (the first plan-phase output produced)
239
- * - 1-design → design-system.md
240
- * - 0-proposal → proposal.md
241
- *
242
- * If no canonical file is found, returns `'proposal'` (the starting phase).
243
- *
244
- * @param {string} featureName
245
- * @param {string} [projectPath]
246
- * @returns {string} Phase name
247
- */
248
- export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
249
-
250
- /**
251
- * The canonical artifact of each phase, descending. Exported so a caller that
252
- * holds the file list from somewhere OTHER than the local filesystem (a git
253
- * tree, say) can answer the same question without re-encoding the map.
254
- */
128
+ export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
129
+
130
+ // Descending. An empty folder is not an artifact: a pre-created folder must
131
+ // not advance the phase.
255
132
  export const PHASE_ARTIFACTS = [
256
- ['4-review', 'review', 'review-report.md'],
257
- ['3-implement', 'implement', 'recap.md'],
258
- ['2-plan', 'plan', 'spec.md'],
133
+ ['4-review', 'review', 'verifier.md'],
134
+ ['3-implement', 'implement', 'reports/'],
259
135
  ['1-design', 'uiux', 'design-system.md'],
260
- ['0-proposal', 'proposal', 'proposal.md'],
136
+ ['2-plan', 'plan', 'plan.md'],
261
137
  ];
262
138
 
139
+ const DESIGN_DIR = '1-design';
140
+
263
141
  const gatesOf = (feature) => feature?.approvals || feature?.approvalGates;
264
142
 
265
- /**
266
- * Gate-implied phase from an ALREADY-LOADED feature object — no disk access.
267
- *
268
- * Split out of `gateImpliedPhase` so callers holding a feature.json that did
269
- * not come from the working tree (e.g. `git show morph/{f}:…`) can reuse the
270
- * rule instead of copying it. The path-based wrapper below is unchanged.
271
- *
272
- * @param {Object|null} feature
273
- * @returns {string} phase name
274
- */
275
- export function gateImpliedPhaseFrom(feature) {
143
+ export function gateImpliedPhaseFrom(feature, designStarted = false) {
276
144
  const gates = gatesOf(feature);
277
145
  const ok = (g) => gates?.[g]?.approved === true;
278
146
  if (ok('review')) return 'review';
279
- if (ok('plan')) return 'implement';
280
- if (ok('uiux')) return 'plan';
281
- return 'proposal';
147
+ if (ok('plan')) return designStarted && !ok('uiux') ? 'uiux' : 'implement';
148
+ return 'plan';
149
+ }
150
+
151
+ /** A path ending in `/` asks for any `.md` file directly under it. */
152
+ export function diskArtifactProbe(featureDir) {
153
+ return (rel) => {
154
+ const abs = join(featureDir, ...rel.split('/').filter(Boolean));
155
+ if (!rel.endsWith('/')) return existsSync(abs);
156
+ try {
157
+ return readdirSync(abs, { withFileTypes: true }).some((e) => e.isFile() && e.name.endsWith('.md'));
158
+ } catch {
159
+ return false;
160
+ }
161
+ };
282
162
  }
283
163
 
284
- /**
285
- * Phase from an already-loaded feature plus an artifact predicate — no disk
286
- * access of its own, so the artifact universe can be a git tree.
287
- *
288
- * @param {Object|null} feature
289
- * @param {(relPath: string) => boolean} hasArtifact - receives POSIX-style
290
- * paths relative to the feature dir, e.g. `2-plan/spec.md`
291
- * @returns {string} phase name
292
- */
164
+ export function fileSetArtifactProbe(files) {
165
+ return (rel) => {
166
+ if (!rel.endsWith('/')) return files.has(rel);
167
+ for (const f of files) {
168
+ if (f.startsWith(rel) && f.endsWith('.md') && !f.slice(rel.length).includes('/')) return true;
169
+ }
170
+ return false;
171
+ };
172
+ }
173
+
174
+ /** @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe} or {@link fileSetArtifactProbe} */
293
175
  export function derivePhaseFrom(feature, hasArtifact) {
294
- let artifactPhase = 'proposal';
176
+ let artifactPhase = 'plan';
295
177
  for (const [folder, phase, canonical] of PHASE_ARTIFACTS) {
296
178
  if (hasArtifact(`${folder}/${canonical}`)) { artifactPhase = phase; break; }
297
179
  }
298
180
 
299
- // Gate-aware: an approved gate advances the phase even before the next
300
- // phase's first canonical artifact exists. This breaks the chicken-and-egg
301
- // where that first artifact could never be written because the phase never
302
- // advanced to permit it. Take whichever of (artifact, gate-implied) is later.
303
- const implied = gateImpliedPhaseFrom(feature);
181
+ // An approved gate advances the phase before the next artifact exists, or
182
+ // that artifact could never be written.
183
+ const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
184
+ const implied = gateImpliedPhaseFrom(feature, designStarted);
304
185
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
305
186
  }
306
187
 
307
- /**
308
- * The phase implied purely by approved gates, independent of which artifacts
309
- * exist on disk. An approved gate authorizes the NEXT phase, so:
310
- * review approved → review (terminal)
311
- * plan approved → implement
312
- * uiux approved → plan
313
- * otherwise → proposal (proposal-approval's fork between uiux/plan is
314
- * resolved by enforce-phase-writes, not here)
315
- * @returns {string} phase name
316
- */
317
188
  export function gateImpliedPhase(featureName, projectPath) {
318
- return gateImpliedPhaseFrom(getFeature(featureName, projectPath));
189
+ const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
190
+ return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
319
191
  }
320
192
 
321
193
  export function derivePhaseForFeature(featureName, projectPath) {
322
194
  const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
323
- return derivePhaseFrom(
324
- getFeature(featureName, projectPath),
325
- (rel) => existsSync(join(basePath, rel)),
326
- );
195
+ return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
327
196
  }
328
197
 
329
- /**
330
- * Get the current phase of a feature.
331
- * Falls back to filesystem derivation when phase is not in state.
332
- * @param {string} featureName
333
- * @param {string} [projectPath]
334
- * @returns {string|null}
335
- */
336
198
  export function getFeaturePhase(featureName, projectPath) {
337
199
  const feature = getFeature(featureName, projectPath);
338
200
  if (!feature) return null;
339
201
  return feature.phase || derivePhaseForFeature(featureName, projectPath);
340
202
  }
341
203
 
342
- /**
343
- * Get approval status for a gate.
344
- * @param {string} featureName
345
- * @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
346
- * @param {string} [projectPath]
347
- * @returns {boolean}
348
- */
349
204
  export function isGateApproved(featureName, gate, projectPath) {
350
205
  const feature = getFeature(featureName, projectPath);
351
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
352
206
  const gates = feature?.approvals || feature?.approvalGates;
353
207
  return gates?.[gate]?.approved === true;
354
208
  }
355
209
 
356
- /**
357
- * Get all pending approval gates for a feature.
358
- * @param {string} featureName
359
- * @param {string} [projectPath]
360
- * @returns {string[]} List of gate names that are not yet approved
361
- */
362
210
  export function pendingGatesFrom(feature) {
363
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
364
211
  const gates = gatesOf(feature);
365
212
  if (!gates) return [];
366
213
 
@@ -373,54 +220,11 @@ export function getPendingGates(featureName, projectPath) {
373
220
  return pendingGatesFrom(getFeature(featureName, projectPath));
374
221
  }
375
222
 
376
- /**
377
- * Get feature outputs status.
378
- * @param {string} featureName
379
- * @param {string} [projectPath]
380
- * @returns {Object|null} outputs map or null
381
- */
382
223
  export function getOutputs(featureName, projectPath) {
383
224
  const feature = getFeature(featureName, projectPath);
384
225
  return feature?.outputs || null;
385
226
  }
386
227
 
387
- /**
388
- * Get uncompleted required outputs for current phase.
389
- * @param {string} featureName
390
- * @param {string} [projectPath]
391
- * @returns {Array<{type: string, path: string}>}
392
- */
393
- /**
394
- * Get task statistics directly from taskList (preferred over feature.tasks counters).
395
- * Excludes expanded tasks from counts, matching calculateProgress() behavior.
396
- *
397
- * @param {Object} feature - Feature state object
398
- * @returns {{ total: number, completed: number, inProgress: number, pending: number }}
399
- */
400
- export function getTaskStats(feature) {
401
- const list = feature?.taskList || [];
402
- if (list.length > 0) {
403
- const active = list.filter(t => t.status !== 'expanded');
404
- return {
405
- total: active.length,
406
- completed: active.filter(t => t.status === 'completed').length,
407
- inProgress: active.filter(t => t.status === 'in_progress').length,
408
- pending: active.filter(t => t.status === 'pending').length,
409
- };
410
- }
411
- // Fallback: read from legacy counter object
412
- const tasks = feature?.tasks;
413
- if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)) {
414
- return {
415
- total: tasks.total || 0,
416
- completed: tasks.completed || 0,
417
- inProgress: tasks.inProgress || 0,
418
- pending: tasks.pending || 0,
419
- };
420
- }
421
- return { total: 0, completed: 0, inProgress: 0, pending: 0 };
422
- }
423
-
424
228
  export function getMissingOutputs(featureName, projectPath) {
425
229
  const feature = getFeature(featureName, projectPath);
426
230
  if (!feature?.outputs) return [];