@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,189 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PreToolUse Hook: Protect Approved Spec Files
5
- *
6
- * Event: PreToolUse | Matcher: Write|Edit
7
- *
8
- * Blocks Write/Edit to spec artifacts after their approval gate has passed:
9
- * - 2-plan/spec.md, etc. → blocked if 'plan' gate approved
10
- * - 1-design/design-system.md, etc. → blocked if 'uiux' gate approved
11
- *
12
- * Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
13
- * the plan contract is frozen, but per-task progress fields (status, outputs,
14
- * notes) may still change during the implement phase. For those files BOTH a
15
- * full Write and a targeted Edit are allowed — but only when the semantic diff
16
- * touches solely the mutable fields. The Edit is simulated (old_string →
17
- * new_string applied to the current file) and diffed exactly like a Write, so a
18
- * one-field status flip via Edit is permitted while any change to the frozen
19
- * plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
20
- * This is the sanctioned progress path — the agent never has to rewrite the
21
- * whole file nor reach for a Node script to record task status.
22
- *
23
- * A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
24
- * exist on disk is a CREATION and passes — the criterion is the file's
25
- * existence, never who signed the gate (see the carve-out in run()).
26
- *
27
- * Fail-open: exits 0 on any error.
28
- */
29
-
30
3
  import { existsSync, readFileSync } from 'fs';
31
4
  import { stateExists, isGateApproved } from '../../shared/state-reader.js';
32
5
  import {
33
6
  isFeaturePath,
34
7
  extractFeatureName,
35
8
  getBasename,
36
- PROTECTED_SPEC_FILES,
37
- MUTABLE_SPEC_FIELDS
9
+ PROTECTED_SPEC_FILES
38
10
  } from '../../shared/phase-utils.js';
39
11
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
40
12
  import { logHookActivity } from '../../shared/activity-logger.js';
41
13
  import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
42
- import { validateTasksDoc, outputsDivergenceViolations } from '../../shared/tasks-validator.js';
43
-
44
- /**
45
- * Compare two tasks.json structures and return the list of plan-contract
46
- * violations — changes that touch anything other than the mutable fields.
47
- *
48
- * Frozen-by-default, not an enumerated frozen list: the loop below walks the
49
- * UNION of old/new task keys and flags any key not in `mutableFields`. A new
50
- * plan-contract field (e.g. `agent`/`standards`, stamped by morph-plan — R2
51
- * token-cut) needs NO entry added here to become protected; it is frozen the
52
- * moment it exists on a task, simply by never being added to
53
- * MUTABLE_SPEC_FIELDS (output-schema.js → `npm run generate` →
54
- * framework/hooks/shared/phase-utils.js). Only fields that must stay
55
- * EDITABLE post-Gate-2 (today: status, outputs, notes) belong in that list.
56
- *
57
- * @param {object} oldDoc - parsed previous file content
58
- * @param {object} newDoc - parsed proposed file content
59
- * @param {string[]} mutableFields - field names allowed to change
60
- * @returns {string[]} human-readable violation messages (empty = OK)
61
- */
62
- function diffPlanContract(oldDoc, newDoc, mutableFields) {
63
- const violations = [];
64
- const mutable = new Set(mutableFields);
65
-
66
- // Backfilling a previously-absent `version` (undefined → any value) is a
67
- // benign schema-tooling update, not a plan-contract change — it's what
68
- // lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
69
- // it without a lenient fallback. An actual change between two SET
70
- // versions is still a real contract change and stays blocked.
71
- const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
72
- if (!versionBackfilled && oldDoc.version !== newDoc.version) {
73
- violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
74
- }
75
- if (oldDoc.feature !== newDoc.feature) {
76
- violations.push(`'feature' changed (${oldDoc.feature} → ${newDoc.feature})`);
77
- }
78
-
79
- const oldTasks = Array.isArray(oldDoc.tasks) ? oldDoc.tasks : [];
80
- const newTasks = Array.isArray(newDoc.tasks) ? newDoc.tasks : [];
81
-
82
- if (oldTasks.length !== newTasks.length) {
83
- violations.push(`task count changed (${oldTasks.length} → ${newTasks.length}) — tasks cannot be added or removed`);
84
- }
85
-
86
- const oldById = new Map(oldTasks.map(t => [t.id, t]));
87
- const newById = new Map(newTasks.map(t => [t.id, t]));
88
-
89
- // A task present before must still exist (no removals / id renames).
90
- for (const id of oldById.keys()) {
91
- if (!newById.has(id)) violations.push(`task '${id}' was removed`);
92
- }
93
- for (const id of newById.keys()) {
94
- if (!oldById.has(id)) violations.push(`task '${id}' was added`);
95
- }
14
+ import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
96
15
 
97
- // For tasks present on both sides, only mutable fields may differ.
98
- for (const [id, newTask] of newById) {
99
- const oldTask = oldById.get(id);
100
- if (!oldTask) continue;
101
- const keys = new Set([...Object.keys(oldTask), ...Object.keys(newTask)]);
102
- for (const key of keys) {
103
- if (mutable.has(key)) continue;
104
- if (JSON.stringify(oldTask[key]) !== JSON.stringify(newTask[key])) {
105
- violations.push(`task '${id}': immutable field '${key}' changed`);
106
- }
107
- }
108
- }
109
-
110
- return violations;
111
- }
112
-
113
- /**
114
- * O heading que abre o bloco de errata, no fim de um arquivo congelado.
115
- *
116
- * Congelar o CONTRATO não é a mesma coisa que congelar um ERRO DE FATO. Caso
117
- * real: a spec afirmava, entre parênteses, que certos registros também eram
118
- * escritos por humanos; o código mostrava que os três escritores eram do
119
- * próprio motor. A afirmação era falsa sobre o código, não uma mudança de
120
- * escopo — mas a trava cega não distingue as duas coisas, então a mentira
121
- * ficava na spec e a correção morava em outro arquivo. Quem abrisse a spec
122
- * primeiro lia a versão errada.
123
- *
124
- * A errata resolve isso SEM abrir o contrato: o corpo continua byte-idêntico e
125
- * só se acrescenta texto ao fim, num bloco identificado. Escopo negociado ⇒
126
- * revogar o gate. Fato errado sobre o código ⇒ errata.
127
- */
128
- const ERRATA_HEADING = '## Errata';
129
-
130
- /** Índice onde começa o bloco de errata (-1 se não há). Só reconhece o heading em início de linha. */
131
- function errataIndex(text) {
132
- if (text.startsWith(ERRATA_HEADING)) return 0;
133
- const at = text.indexOf(`\n${ERRATA_HEADING}`);
134
- return at === -1 ? -1 : at + 1;
135
- }
136
-
137
- /**
138
- * Corpo congelado = tudo ANTES do bloco de errata (ou o texto inteiro, se não
139
- * há bloco), sem o branco final.
140
- *
141
- * O `trimEnd` não é frouxidão: separar o bloco por uma linha em branco é o
142
- * formato markdown natural, e sem isso a primeira errata de todas seria
143
- * recusada por "mudou o corpo" — quando o que mudou foi um `\n` de formatação.
144
- * A garantia que importa (nenhum caractere SIGNIFICATIVO do texto aprovado
145
- * muda) fica intacta, porque só espaço em branco terminal é ignorado.
146
- */
147
- function frozenBody(text) {
148
- const at = errataIndex(text);
149
- return (at === -1 ? text : text.slice(0, at)).trimEnd();
150
- }
151
-
152
- /**
153
- * A mudança proposta é uma errata legítima?
154
- *
155
- * Três condições, todas necessárias — frouxo em qualquer uma delas e a trava
156
- * cega vira uma trava que uma edição criativa atravessa:
157
- * 1. o corpo congelado permanece BYTE-IDÊNTICO;
158
- * 2. o resultado de fato tem um bloco `## Errata` (senão é edição do corpo);
159
- * 3. a errata só CRESCE — o que já estava escrito nela não é reescrito.
160
- *
161
- * A (3) existe porque uma errata reescrita perde a serventia: o valor dela é
162
- * ser trilha acumulada, não um segundo lugar editável.
163
- *
164
- * @returns {{ok: true} | {ok: false, why: string}}
165
- */
166
- function errataViolation(original, proposed) {
167
- if (frozenBody(proposed) !== frozenBody(original)) {
168
- return { ok: false, why: 'o corpo da spec mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
169
- }
170
- if (errataIndex(proposed) === -1) {
171
- return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
172
- }
173
- const oldAt = errataIndex(original);
174
- if (oldAt !== -1) {
175
- const oldErrata = original.slice(oldAt);
176
- if (!proposed.slice(errataIndex(proposed)).startsWith(oldErrata)) {
177
- return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
178
- }
179
- }
180
- return { ok: true };
181
- }
16
+ const CHECKS_FILE = 'checks.json';
182
17
 
183
- /**
184
- * Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
185
- * @returns {string|null} null quando o payload não permite reconstruir (fail-open).
186
- */
187
18
  function proposedText(payload, toolName, original) {
188
19
  if (toolName === 'Edit') {
189
20
  const oldStr = payload?.tool_input?.old_string;
@@ -197,14 +28,8 @@ function proposedText(payload, toolName, original) {
197
28
  return typeof content === 'string' ? content : null;
198
29
  }
199
30
 
200
- /**
201
- * @param {object} payload - parsed PreToolUse stdin payload
202
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
203
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
204
- */
205
31
  export async function run(payload) {
206
32
  try {
207
- // Amend mode: bypass spec protection for legitimate in-implementation corrections
208
33
  if (process.env.MORPH_AMEND_PHASE) {
209
34
  console.error(`[morph-spec] AMEND MODE: bypassing spec protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
210
35
  return null;
@@ -215,14 +40,9 @@ export async function run(payload) {
215
40
  const filePath = getFilePath(payload);
216
41
  if (!filePath) return null;
217
42
 
218
- // Only check files inside .morph/features/
219
43
  if (!isFeaturePath(filePath)) return null;
220
44
 
221
- // The gate that locks this file is recorded in the feature.json of the tree
222
- // that OWNS it. Asking the session's tree instead made this guard go DARK in
223
- // a secondary worktree: the root had no approved gate, so a post-Gate-2
224
- // spec.md rewrite sailed through. Same defect as enforce-phase-writes,
225
- // opposite direction — there it over-blocked, here it under-protected.
45
+ // The gate lives in the feature.json of the tree that OWNS the file: asking the session's tree left this guard dark in a worktree.
226
46
  const root = resolveMorphRoot(filePath, process.cwd());
227
47
  if (!stateExists(root)) return null;
228
48
 
@@ -232,133 +52,42 @@ export async function run(payload) {
232
52
  const filename = getBasename(filePath);
233
53
  const requiredGate = PROTECTED_SPEC_FILES[filename];
234
54
 
235
- // Not a protected file
236
55
  if (!requiredGate) return null;
237
56
 
238
- // Gate not yet approved — allow editing
57
+ // Generated only by the CLI: a hand-written one would carry checks the parser never judged.
58
+ // Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
59
+ if (filename === CHECKS_FILE) {
60
+ logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
61
+ return blockResult(
62
+ `MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
63
+ `Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
64
+ `After the plan gate the plan is frozen: a wrong fact goes in \`${ERRATA_HEADING}\` (append-only, ${CHECKS_FILE} unchanged);\n` +
65
+ `a real scope change needs \`morph-spec unapprove ${featureName} plan\`, then plan-check and the gate again.`
66
+ );
67
+ }
68
+
239
69
  if (!isGateApproved(featureName, requiredGate, root)) {
240
70
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
241
71
  return null;
242
72
  }
243
73
 
244
- // ── Gate approved — file is locked ──────────────────────────────────────
245
74
  const absPath = resolveTargetPath(filePath, root);
246
75
 
247
- // Um gate aprovado congela o ARTEFATO APROVADO. Se o arquivo ainda não
248
- // existe no disco, não há artefato aprovado a proteger: isto é a CRIAÇÃO
249
- // inicial, não a edição de uma spec fechada. O caso é real e não é raro —
250
- // `chore`/`hotfix` nascem com `proposal`+`plan` carimbados `policy:{tipo}`
251
- // (create.js → work-types.js skippedGates), então o primeiro `Write` de
252
- // `2-plan/spec.md` já chega aqui com o gate "aprovado" sem que humano
253
- // nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
254
- // mandando revogar o gate de um arquivo inexistente.
255
- //
256
- // O critério é a EXISTÊNCIA, nunca o `approvedBy`. Liberar por assinatura
257
- // (`policy:` passa, `lucas` não) transformaria este carve-out em licença
258
- // para reescrever qualquer spec aprovada: bastaria apagá-la antes. Por ser
259
- // sobre o disco, a janela fecha sozinha no instante em que o arquivo
260
- // existe — os contra-canários de test/hooks/protect-spec-files.test.js
261
- // pinam exatamente isso.
262
- //
263
- // A decisão é explícita e vale para as DUAS metades do hook. Antes, o ramo
264
- // de campos mutáveis (tasks.json) já deixava a primeira criação passar, mas
265
- // por acidente — o ENOENT caía no catch fail-open da leitura — enquanto o
266
- // ramo travado bloqueava. Duas metades respondendo à mesma pergunta em
267
- // direções opostas; agora respondem aqui, uma vez só. O `result` distinto
268
- // no log é o que torna essa unificação observável.
76
+ // A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
77
+ // The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
269
78
  if (!existsSync(absPath)) {
270
79
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
271
80
  return null;
272
81
  }
273
82
 
274
- const mutableFields = MUTABLE_SPEC_FIELDS[filename];
275
-
276
- // Files with mutable progress fields (e.g. tasks.json): allow a full Write
277
- // that touches only those fields. Anything else stays blocked.
278
- if (mutableFields) {
279
- const toolName = getToolName(payload);
280
-
281
- // Build the proposed document from either a full Write or a targeted Edit,
282
- // then semantic-diff it against the current file on disk. Any failure to
283
- // read / parse / apply → fail-open (pass), never a false block. O arquivo
284
- // EXISTE aqui (a inexistência já foi decidida acima), então este catch
285
- // volta a significar só o que dizia originalmente: não deu para diffar.
286
- let oldDoc, newDoc;
287
- try {
288
- const original = readFileSync(absPath, 'utf-8');
289
- oldDoc = JSON.parse(original);
290
-
291
- if (toolName === 'Edit') {
292
- // Simulate the edit: apply old_string → new_string to the current file.
293
- const oldStr = payload?.tool_input?.old_string;
294
- const newStr = payload?.tool_input?.new_string;
295
- if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
296
- const edited = payload?.tool_input?.replace_all
297
- ? original.split(oldStr).join(newStr)
298
- : original.replace(oldStr, newStr);
299
- newDoc = JSON.parse(edited);
300
- } else {
301
- const newContent = payload?.tool_input?.content;
302
- if (typeof newContent !== 'string') throw new Error('no content');
303
- newDoc = JSON.parse(newContent);
304
- }
305
- } catch {
306
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
307
- return null;
308
- }
309
-
310
- const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
311
-
312
- // Schema check on the proposed doc: even mutable-field writes must keep a
313
- // valid status enum (a leaf agent writing status:"completed" was the
314
- // production failure this closes). Only NEWLY introduced schema errors
315
- // block — a pre-existing violation must not deadlock progress writes.
316
- const preExisting = new Set(validateTasksDoc(oldDoc).errors);
317
- violations.push(...validateTasksDoc(newDoc).errors.filter(e => !preExisting.has(e)));
318
-
319
- // Planned-vs-real outputs diff: changing `outputs` is allowed, but the
320
- // divergence must be explained in `notes` in the same edit.
321
- violations.push(...outputsDivergenceViolations(oldDoc, newDoc));
322
-
323
- if (violations.length === 0) {
324
- // Only mutable progress fields changed — a legitimate implement-phase
325
- // update via Write OR Edit.
326
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
327
- return null;
328
- }
329
-
330
- logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
331
- return blockResult(
332
- `MORPH-SPEC: '${filename}' write rejected — the '${requiredGate}' gate has been approved.\n` +
333
- `This ${toolName} violates the plan contract or the tasks schema:\n` +
334
- violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
335
- `During the implement phase only these fields may change: ${mutableFields.join(', ')}\n` +
336
- `(status must stay in the schema enum, and outputs that diverge from the plan\n` +
337
- `require an explanatory 'notes' in the same edit).\n` +
338
- `To change the plan itself, ask the user to revoke the '${requiredGate}' gate,\n` +
339
- `then re-present it for approval.`
340
- );
341
- }
342
-
343
- // ── Arquivo totalmente travado (spec.md, design) ────────────
344
- // O corpo continua congelado. A ÚNICA passagem é a errata append-only:
345
- // corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
346
- // de leitura/reconstrução cai no bloqueio (o conservador aqui é BLOQUEAR —
347
- // ao contrário do ramo do tasks.json, onde não conseguir diffar significa
348
- // não ter como provar violação; aqui a regra base é "não se edita").
349
- //
350
- // O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
351
- // é a única coisa que este catch NÃO significa mais. ENOENT tratado como
352
- // "ilegível" era o defeito F14; "ilegível de verdade" continua bloqueando.
83
+ // The only way through is the append-only errata; any read or rebuild failure falls into the block.
353
84
  const toolName = getToolName(payload);
354
85
  let errataCheck = null;
355
86
  try {
356
87
  const original = readFileSync(absPath, 'utf-8');
357
88
  const proposed = proposedText(payload, toolName, original);
358
89
  if (proposed !== null) errataCheck = errataViolation(original, proposed);
359
- } catch {
360
- // arquivo ilegível → sem errata possível, segue para o bloqueio
361
- }
90
+ } catch {}
362
91
 
363
92
  if (errataCheck?.ok) {
364
93
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
@@ -368,16 +97,15 @@ export async function run(payload) {
368
97
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
369
98
  return blockResult(
370
99
  `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
371
- `Editing approved specs breaks the spec contract.\n\n` +
100
+ `Editing an approved artifact breaks its contract.\n\n` +
372
101
  (errataCheck ? `Esta mudança não é uma errata válida: ${errataCheck.why}.\n\n` : '') +
373
- `Corrigir um FATO ERRADO sobre o código (a spec afirma algo que o código\n` +
374
- `desmente) não exige revogar o gate: acrescente um bloco \`${ERRATA_HEADING}\` no\n` +
102
+ `Corrigir um FATO ERRADO sobre o código (o texto aprovado afirma algo que o\n` +
103
+ `código desmente) não exige revogar o gate: acrescente sob \`${ERRATA_HEADING}\` no\n` +
375
104
  `FIM do arquivo, deixando o corpo intacto — é append-only e fica na trilha.\n\n` +
376
- `Mudar o ESCOPO acordado é outra coisa: peça ao usuário para revogar o gate\n` +
377
- `'${requiredGate}', edite, e re-apresente o gate para aprovação.`
105
+ `Mudar o ESCOPO acordado é outra coisa: \`morph-spec unapprove ${featureName} ${requiredGate}\`,\n` +
106
+ `edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
378
107
  );
379
108
  } catch {
380
- // Fail-open
381
109
  return null;
382
110
  }
383
111
  }
@@ -1,15 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * state-sync.js — PostToolUse hook
4
- *
5
- * Keeps .morph/state.json in sync with the agent's actions, without requiring
6
- * the agent to think about state. Fail-open: any error returns {} silently.
7
- *
8
- * Triggers:
9
- * - Write/Edit of .morph/features/{feature}/0-proposal/proposal.md → register feature
10
- * - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
11
- * - AskUserQuestion answered with option starting "aprovado" → advance gate
12
- */
13
2
 
14
3
  import { existsSync } from 'fs';
15
4
  import { join } from 'path';
@@ -17,6 +6,7 @@ import { loadState, stateExists } from '../../shared/state-reader.js';
17
6
  import { persistFeature, setGateApproved, createDefaultFeature, readFeatureFile } from '../../shared/state-writer.js';
18
7
  import { resolveMorphRoot } from '../../shared/payload-utils.js';
19
8
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
9
+ import { gateFromQuestion } from '../../shared/gate-question.js';
20
10
  import {
21
11
  locateFeatureForQuestion,
22
12
  allWorkTrees,
@@ -25,9 +15,8 @@ import {
25
15
  } from '../../shared/feature-locator.js';
26
16
 
27
17
  const PHASE_FOLDERS = {
28
- '0-proposal': 'proposal',
29
18
  '1-design': 'design',
30
- '1-ui': 'uiux', // legacy alias — still recognized during folder transition
19
+ '1-ui': 'uiux', // legacy alias
31
20
  '2-plan': 'plan',
32
21
  '3-implement': 'implement',
33
22
  '4-review': 'review',
@@ -62,8 +51,6 @@ function registerFeatureIfMissing(state, featureName) {
62
51
  if (!state.features) state.features = {};
63
52
  if (state.features[featureName]) return false;
64
53
 
65
- // Canonical object-form gates (incl. optional uiux) + stamped identity
66
- // (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
67
54
  state.features[featureName] = createDefaultFeature(featureName);
68
55
  return true;
69
56
  }
@@ -94,24 +81,8 @@ function isApprovalAnswer(answer) {
94
81
  );
95
82
  }
96
83
 
97
- function gateFromQuestion(question) {
98
- if (typeof question !== 'string') return null;
99
- // Checked BEFORE the generic patterns below: the pinned uiux template is
100
- // "Gate uiux — Design de {feature} aprovado?" (morph-uiux/SKILL.md) — it
101
- // must never fall through to 'plan' via a stray "plan"/"plano" substring in
102
- // the feature name. `/gate\s+uiux/i` alone is unambiguous either way.
103
- if (/gate\s+uiux/i.test(question)) return 'uiux';
104
- const lower = question.toLowerCase();
105
- if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
106
- if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
107
- if (lower.includes('gate 3') || lower.includes('review')) return 'review';
108
- return null;
109
- }
110
-
111
- // Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
112
- // installed standalone and cannot import from src/). Keep in sync:
113
- // proposal → plan → review; uiux requires proposal.
114
- const GATE_PREREQ = { proposal: [], uiux: ['proposal'], plan: ['proposal'], review: ['plan'] };
84
+ // Mirror of src/core/state/gate-order.js: hooks are installed standalone and cannot import src/.
85
+ const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
115
86
  function unmetGatePrereqs(gate, gates) {
116
87
  const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
117
88
  return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
@@ -122,9 +93,7 @@ function handleWriteEdit(input, sessionRoot) {
122
93
  const featureName = extractFeatureName(filePath);
123
94
  if (!featureName) return false;
124
95
 
125
- // Sync the tree that OWNS the written file. Syncing the session's tree
126
- // instead registered the feature (and its outputs) in the wrong `.morph/`
127
- // whenever a root-rooted session wrote into a secondary worktree.
96
+ // Sync the tree that OWNS the written file: a root session also writes into worktrees.
128
97
  const cwd = resolveMorphRoot(filePath, sessionRoot);
129
98
  if (!stateExists(cwd)) return false;
130
99
 
@@ -139,8 +108,6 @@ function handleWriteEdit(input, sessionRoot) {
139
108
  }
140
109
 
141
110
  if (changed) {
142
- // Persist the touched feature to its authoritative feature.json and refresh
143
- // the thin root index. The feature's state thus travels in its own folder.
144
111
  persistFeature(featureName, state.features[featureName], cwd);
145
112
  }
146
113
  return true;
@@ -156,22 +123,11 @@ function handleAskUserQuestion(input, sessionRoot) {
156
123
  const gate = gateFromQuestion(question);
157
124
  if (!gate) continue;
158
125
 
159
- // Attribute the approval to the feature the QUESTION names, in the tree
160
- // that owns it — never to "the first in_progress feature of whatever tree
161
- // the session sits in", which is how a Gate 2 for one feature was stamped
162
- // onto `export-hub`, leaving it with G2 approved and G1 never asked.
126
+ // The feature the QUESTION names, in the tree that owns it — never the session tree's first in-progress feature.
163
127
  const located = locateFeatureForQuestion(question, sessionRoot);
164
128
  if (!located) {
165
- // Refuse to guess. A gate on the wrong feature inverts gate order and
166
- // invites a second stamp on the real owner; writing nothing costs one
167
- // explicit remediation step.
168
- //
169
- // The remediation differs by WHY it refused. When the question names a
170
- // specific slug that no known tree has registered (a hotfix gate asked
171
- // before `create` ran — the case that used to fall through to "the
172
- // tree's one feature"), `morph-spec approve <that-name> <gate>` is a
173
- // dead end: approve.js does `getFeature()` and hard-refuses "Feature
174
- // not found". Name the real fix instead of the wrong command.
129
+ // Refuse to guess: a gate on the wrong feature inverts gate order. For an unregistered slug
130
+ // `approve` is a dead end ("Feature not found"), so the remediation names `create`.
175
131
  const allNames = allWorkTrees(sessionRoot).flatMap((r) => featureNamesIn(r));
176
132
  const candidate = unrecognizedSlugLike(question, allNames);
177
133
  const remediation = candidate
@@ -190,20 +146,14 @@ function handleAskUserQuestion(input, sessionRoot) {
190
146
  const { name: featureName, root } = located;
191
147
  if (!stateExists(root)) continue;
192
148
 
193
- // Prefer the authoritative feature.json; fall back to loadState's merged
194
- // view so a feature carried only by the thin index still reports its real
195
- // gates (otherwise every predecessor reads as unapproved and the
196
- // out-of-order warning below fires on a perfectly ordered approval).
149
+ // Fallback to the merged view: a feature only in the thin index would read every predecessor as unapproved.
197
150
  const feature = readFeatureFile(featureName, root) || loadState(root)?.features?.[featureName];
198
151
  const gates = feature?.approvalGates;
199
152
  const existing = gates?.[gate];
200
153
  const alreadyApproved = existing === true || existing?.approved === true;
201
154
  if (alreadyApproved) continue;
202
155
 
203
- // Gate monotonicity: fail-open. The human said "aprovado", so we DO approve
204
- // (never block a human in the loop), but if a predecessor gate is still
205
- // pending we surface a non-blocking warning — the CLI `morph-spec approve`
206
- // hard-refuses the same shape; here we alert so the operator can reconcile.
156
+ // Out of order still approves (never block a human in the loop); `morph-spec approve` refuses the same shape.
207
157
  const unmet = unmetGatePrereqs(gate, gates);
208
158
  if (unmet.length) {
209
159
  warnings.push(
@@ -213,18 +163,12 @@ function handleAskUserQuestion(input, sessionRoot) {
213
163
  );
214
164
  }
215
165
 
216
- // An "aprovado" answer to AskUserQuestion is a human approving in the loop.
217
- // Route through the shared canonical gate writer so the hook and the CLI
218
- // (`morph-spec approve`) produce one consistent shape
219
- // ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
220
- // and a manual approval stays distinguishable from a policy auto-approval.
221
- // Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
222
- // independent evaluator, but the degradation is stamped, never silent.
166
+ // Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
223
167
  const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
224
- && !existsSync(join(root, '.morph', 'features', featureName, '4-review', 'evaluator-report.md'));
168
+ && ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
225
169
  if (selfAssessed) {
226
170
  warnings.push(
227
- `⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM avaliador independente — ` +
171
+ `⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
228
172
  `registrado como selfAssessed (aparece no list/fleet).`
229
173
  );
230
174
  }
@@ -233,19 +177,9 @@ function handleAskUserQuestion(input, sessionRoot) {
233
177
  return warnings.length ? warnings.join('\n') : null;
234
178
  }
235
179
 
236
- /**
237
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
238
- * @returns {Promise<object|null>} a hookSpecificOutput warning on a gate
239
- * order/attribution violation, or null (pass/silent).
240
- */
241
180
  export async function run(payload) {
242
181
  try {
243
- // The SESSION's root. It is only a starting point: which `.morph/` actually
244
- // gets read and written is decided per event — by the target path for
245
- // Write/Edit, by the feature the question names for AskUserQuestion. The
246
- // `stateExists` gate therefore moved into each handler, against the tree that
247
- // handler resolved; gating on the session's tree up here would skip work that
248
- // belongs to a worktree (and, worse, silently pass it to the wrong one).
182
+ // Only a starting point: each handler resolves the tree it reads and writes, so the stateExists gate lives there.
249
183
  const sessionRoot = projectDir();
250
184
 
251
185
  const input = payload || {};
@@ -258,12 +192,8 @@ export async function run(payload) {
258
192
  } else if (toolName === 'AskUserQuestion') {
259
193
  warning = handleAskUserQuestion(input, sessionRoot);
260
194
  }
261
- } catch {
262
- // fail-open
263
- }
195
+ } catch {}
264
196
 
265
- // A non-blocking order-violation warning is surfaced as injected context
266
- // (hookSpecificOutput.additionalContext); everything else stays silent.
267
197
  return warning ? contextResult(warning, 'PostToolUse') : null;
268
198
  } catch {
269
199
  return null;