@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  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 +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  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 +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  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 +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  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 +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  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 +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,22 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PreToolUse Hook: Enforce Phase-Appropriate File Writes
5
- *
6
- * Event: PreToolUse | Matcher: Write|Edit
7
- *
8
- * Ensures writes to .morph/features/{feature}/ go to the current phase's subfolder:
9
- * plan → only 2-plan/ (and 2-plan/ is writable in every phase: the errata
10
- * and plan-check live there)
11
- * uiux → only 1-design/
12
- * implement → 3-implement/ + any source code (unrestricted)
13
- * review → only 4-review/
14
- *
15
- * Files outside .morph/features/ are always allowed.
16
- *
17
- * Fail-open: exits 0 on any error.
18
- */
19
-
20
3
  import { existsSync } from 'fs';
21
4
  import { stateExists, getFeaturePhase, isGateApproved } from '../../shared/state-reader.js';
22
5
  import {
@@ -26,13 +9,7 @@ import {
26
9
  PHASE_DIRS
27
10
  } from '../../shared/phase-utils.js';
28
11
 
29
- /**
30
- * The gate that authorizes writing a phase folder's FIRST artifact. Once that
31
- * gate is approved, the next phase's canonical file may be created even though
32
- * the phase only advances (via derivation) once that file exists — this is the
33
- * fix for the forward-transition chicken-and-egg. The plan gate unlocks both
34
- * the optional `1-design/` and `3-implement/` (the uiux fork).
35
- */
12
+ // Approving this gate lets the next phase's first file be created: the phase only advances once that file exists.
36
13
  const PHASE_DIR_ENTRY_GATE = {
37
14
  '1-design': 'plan',
38
15
  '3-implement': 'plan',
@@ -44,14 +21,8 @@ import { blockResult, runStandalone } from '../../shared/hook-response.js';
44
21
  import { logHookActivity } from '../../shared/activity-logger.js';
45
22
  import { getFilePath, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
46
23
 
47
- /**
48
- * @param {object} payload - parsed PreToolUse stdin payload
49
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
50
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
51
- */
52
24
  export async function run(payload) {
53
25
  try {
54
- // Amend mode: bypass phase write enforcement for legitimate corrections
55
26
  if (process.env.MORPH_AMEND_PHASE) {
56
27
  console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
57
28
  return null;
@@ -62,14 +33,9 @@ export async function run(payload) {
62
33
  const filePath = getFilePath(payload);
63
34
  if (!filePath) return null;
64
35
 
65
- // Only check files inside .morph/features/
66
36
  if (!isFeaturePath(filePath)) return null;
67
37
 
68
- // Resolve the tree that OWNS this file, NOT the tree the session sits in.
69
- // A session rooted in the primary repo may legitimately write to a feature
70
- // living in a secondary worktree; reading the session's `.morph/` there
71
- // reported the ROOT's phase for the WORKTREE's file and blocked a legal write.
72
- // `stateExists` must be asked of that same tree — a worktree carries its own state.
38
+ // The tree that OWNS the file, not the session's: a root session may write into a worktree's feature.
73
39
  const root = resolveMorphRoot(filePath, process.cwd());
74
40
  if (!stateExists(root)) return null;
75
41
 
@@ -79,28 +45,22 @@ export async function run(payload) {
79
45
  const phase = getFeaturePhase(featureName, root);
80
46
  if (!phase) return null;
81
47
 
82
- // During implement phase, all writes are allowed (source code is unrestricted)
83
- if (phase === 'implement' || phase === 'sync') return null;
48
+ if (phase === 'implement') return null;
84
49
 
85
50
  const targetDir = extractPhaseDir(filePath);
86
- if (!targetDir) return null; // File directly in feature root — allow
51
+ if (!targetDir) return null;
87
52
  if (targetDir === ALWAYS_WRITABLE_DIR) return null;
88
53
 
89
54
  const allowedDir = PHASE_DIRS[phase];
90
- if (!allowedDir) return null; // Unknown phase — allow
55
+ if (!allowedDir) return null;
91
56
 
92
57
  if (targetDir !== allowedDir) {
93
- // Allow editing already-existing files from previous phases
94
58
  const absolutePath = resolveTargetPath(filePath, root);
95
59
  if (existsSync(absolutePath)) {
96
60
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
97
- return null; // editing existing file — allow
61
+ return null;
98
62
  }
99
63
 
100
- // Gate-aware forward transition: allow creating the NEXT phase's first
101
- // artifact as soon as the gate authorizing that phase is approved. Without
102
- // this, the first file of a new phase can never be written (the phase only
103
- // advances once that file exists) — the deadlock this hook used to create.
104
64
  const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
105
65
  if (entryGate && isGateApproved(featureName, entryGate, root)) {
106
66
  // uiux is optional, but once 1-design/ exists its gate is not
@@ -124,9 +84,6 @@ export async function run(payload) {
124
84
  }
125
85
 
126
86
  const phaseLabel = phase.toUpperCase();
127
- // Name the tree the verdict came from whenever it is NOT the session's own.
128
- // The phase read is per-tree, so "which .morph/ said plan?" is the first
129
- // question anyone debugging this asks.
130
87
  const treeNote = root !== process.cwd().replace(/\\/g, '/')
131
88
  ? `\n(phase read from the tree that owns the file: ${root})`
132
89
  : '';
@@ -143,7 +100,6 @@ export async function run(payload) {
143
100
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
144
101
  return null;
145
102
  } catch {
146
- // Fail-open
147
103
  return null;
148
104
  }
149
105
  }
@@ -1,28 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * gate-guard.js — PostToolUse hook (detect-and-warn, never block)
4
- *
5
- * Closes the gap from issue #63: the trust-mode contract lived only in skill
6
- * pseudocode, with nothing verifying the LLM actually honored it. This hook
7
- * compares the OBSERVED gate action against the decision the skill persisted
8
- * to state.json (`features[feature].gateDecisions[gate]`) and injects a
9
- * non-blocking warning when they disagree.
10
- *
11
- * It is a guardrail, NOT an orchestrator (per MORPH.md): it never blocks,
12
- * never writes state, and fails open on any error or missing data.
13
- *
14
- * Two mismatch directions:
15
- * - LLM PAUSED (AskUserQuestion on a gate) but the persisted decision said
16
- * pause:false → over-pausing under trust:auto. Warn.
17
- * - LLM AUTO-APPROVED (gate became approvedBy:"auto") but the persisted
18
- * decision said pause:true → skipped a required human pause. Warn.
19
- *
20
- * Observability: every time a gate action is actually checked against a
21
- * persisted gateDecisions entry, one line lands in the activity log
22
- * (.morph/logs/activity.json) with result 'coherent' or 'divergent' — so a
23
- * human can confirm the guard verified coherence instead of absolute silence.
24
- * No persisted decision, non-gate action, or any error → silence.
25
- */
2
+ // Detect-and-warn, never block: compares the observed gate action with the persisted gateDecisions.
26
3
 
27
4
  import { loadState, stateExists, getActiveFeature } from '../../shared/state-reader.js';
28
5
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
@@ -35,17 +12,12 @@ function projectDir() {
35
12
  return process.env.CLAUDE_PROJECT_DIR || process.cwd();
36
13
  }
37
14
 
38
- /** Feature slug from a `.morph/features/{name}/feature.json` path. */
39
15
  function featureFromStatePath(filePath) {
40
16
  const fp = String(filePath ?? '').replace(/\\/g, '/');
41
17
  return (fp.match(/\.morph\/features\/([^/]+)\/feature\.json$/) ?? [])[1] ?? null;
42
18
  }
43
19
 
44
- /**
45
- * The tree owning an edited gate-state file. `resolveMorphRoot` covers the v5
46
- * per-feature `feature.json`; the legacy `.morph/state.json` has no feature
47
- * segment to anchor on, so strip its own suffix instead.
48
- */
20
+ // The legacy `.morph/state.json` has no feature segment to anchor on, so strip its own suffix.
49
21
  function rootOfGateStateFile(filePath, fallback) {
50
22
  const abs = resolveTargetPath(filePath, fallback);
51
23
  const suffix = '/.morph/state.json';
@@ -53,7 +25,6 @@ function rootOfGateStateFile(filePath, fallback) {
53
25
  return resolveMorphRoot(abs, fallback);
54
26
  }
55
27
 
56
- /** Every question/header text this AskUserQuestion call surfaced. */
57
28
  function askedTexts(input) {
58
29
  const texts = [];
59
30
  const questions = input.tool_input?.questions;
@@ -75,29 +46,20 @@ function getDecision(state, feature, gate) {
75
46
  return d;
76
47
  }
77
48
 
78
- /**
79
- * The LLM just asked a gate question. If the persisted decision said this
80
- * gate should auto-pass (pause:false), the human was interrupted needlessly.
81
- */
82
49
  function checkAskUserQuestion(input, sessionRoot) {
83
50
  for (const text of askedTexts(input)) {
84
51
  const gate = gateFromQuestion(text);
85
52
  if (!gate) continue;
86
53
 
87
- // Check the decision of the feature the question NAMES, in its own tree.
88
- // Reading the session tree's first in_progress feature made this guard
89
- // compare a gate against an unrelated feature's gateDecisions — a warning
90
- // about the wrong thing is worse than silence, so an unattributable
91
- // question is skipped rather than guessed at (state-sync already tells the
92
- // operator when a gate answer could not be attributed).
54
+ // Only the feature the question NAMES: a warning about the wrong feature is worse than silence.
93
55
  const located = locateFeatureForQuestion(text, sessionRoot);
94
56
  if (!located) continue;
95
57
  const { name: feature, root: cwd } = located;
96
- if (!stateExists(cwd)) continue; // fail-open
58
+ if (!stateExists(cwd)) continue;
97
59
 
98
60
  const state = loadState(cwd);
99
61
  const decision = getDecision(state, feature, gate);
100
- if (!decision) continue; // no persisted decision → nothing to verify
62
+ if (!decision) continue;
101
63
  if (decision.pause === false) {
102
64
  logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
103
65
  return contextResult(
@@ -110,34 +72,24 @@ function checkAskUserQuestion(input, sessionRoot) {
110
72
  'PostToolUse'
111
73
  );
112
74
  }
113
- // The decision said pause:true and the LLM did pause — coherent.
114
75
  logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
115
76
  return null;
116
77
  }
117
78
  return null;
118
79
  }
119
80
 
120
- /** True for edits to .morph/state.json (legacy) or a per-feature feature.json (v5). */
121
81
  function isGateStateFile(filePath) {
122
82
  const fp = String(filePath ?? '').replace(/\\/g, '/');
123
83
  if (fp.endsWith('.morph/state.json')) return true;
124
84
  return fp.includes('.morph/features/') && fp.endsWith('/feature.json');
125
85
  }
126
86
 
127
- /**
128
- * The LLM just edited gate state (state.json legacy, or the authoritative
129
- * feature.json in v5). If a gate flipped to approvedBy:"auto" but the
130
- * persisted decision said pause:true, a required human pause was skipped.
131
- */
132
87
  function checkStateEdit(input, sessionRoot) {
133
88
  const filePath = input.tool_input?.file_path;
134
89
  if (!isGateStateFile(filePath)) return null;
135
90
 
136
- // The edited file names its own tree — and, for the v5 per-feature
137
- // feature.json, its own feature. Only the legacy `.morph/state.json` shape
138
- // still needs a most-recently-updated fallback within that tree.
139
91
  const cwd = rootOfGateStateFile(filePath, sessionRoot);
140
- if (!stateExists(cwd)) return null; // fail-open
92
+ if (!stateExists(cwd)) return null;
141
93
  const state = loadState(cwd);
142
94
  if (!state) return null;
143
95
  const feature = featureFromStatePath(filePath) || getActiveFeature(cwd)?.name;
@@ -168,22 +120,12 @@ function checkStateEdit(input, sessionRoot) {
168
120
  return null;
169
121
  }
170
122
 
171
- /**
172
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
173
- * @returns {Promise<object|null>} a hookSpecificOutput warning on a
174
- * trust-mode divergence, or null (pass/silent).
175
- */
176
123
  export async function run(payload) {
177
- // Starting point only — each branch resolves the tree it actually inspects
178
- // (from the edited path, or from the feature the question names).
179
124
  const sessionRoot = projectDir();
180
125
 
181
126
  const input = payload || {};
182
127
  const toolName = input.tool_name;
183
128
 
184
- // Short-circuit before loading state: a Write/Edit only matters if it
185
- // touched gate state (state.json legacy, or the v5 per-feature
186
- // feature.json). Skip the disk read for unrelated edits.
187
129
  if (toolName === 'Edit' || toolName === 'Write') {
188
130
  if (!isGateStateFile(input.tool_input?.file_path)) return null;
189
131
  } else if (toolName !== 'AskUserQuestion') {
@@ -193,9 +135,7 @@ export async function run(payload) {
193
135
  try {
194
136
  if (toolName === 'AskUserQuestion') return checkAskUserQuestion(input, sessionRoot);
195
137
  return checkStateEdit(input, sessionRoot);
196
- } catch {
197
- // fail-open
198
- }
138
+ } catch {}
199
139
  return null;
200
140
  }
201
141
 
@@ -1,20 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: dispara o refresh do grafo de codebase
5
- *
6
- * Event: PostToolUse | Matcher: none (elo da cadeia de todos os tools)
7
- *
8
- * Depois de um comando de shell — Bash ou PowerShell, que no Windows é o shell
9
- * principal do Claude Code — que chama git, gh ou morph-spec, solta o refresh
10
- * do grafo da RAIZ em background (framework/hooks/shared/graph-refresh.js) e
11
- * volta na hora. O filtro é largo de propósito: o refresh só roda quando o
12
- * grafo da raiz está atrás do HEAD, então um `git status` custa um processo
13
- * que sai sem fazer nada — e um comando que move o HEAD sem ser reconhecido é
14
- * exatamente como o grafo ficou 85 commits atrás.
15
- *
16
- * Silencioso e fail-open: nunca injeta contexto, nunca bloqueia.
17
- */
3
+ // Wide filter on purpose: the refresh only runs when the root's graph is behind HEAD, and a HEAD-moving command
4
+ // left unrecognized is how the graph falls behind.
18
5
 
19
6
  import { stateExists } from '../../shared/state-reader.js';
20
7
  import { runStandalone } from '../../shared/hook-response.js';
@@ -24,20 +11,12 @@ import { kickGraphRefresh } from '../../shared/graph-refresh.js';
24
11
  const SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
25
12
  const GIT_TOOL = /(^|[\\/])(git|gh|morph-spec)(\.exe|\.cmd)?$/i;
26
13
 
27
- /**
28
- * @param {string} command
29
- * @returns {boolean} o comando chama git, gh ou morph-spec em algum ponto
30
- */
31
14
  export function touchesGit(command) {
32
15
  return tokenize(command)
33
16
  .flatMap((t) => t.split(/[;&|()]+/))
34
17
  .some((t) => GIT_TOOL.test(t));
35
18
  }
36
19
 
37
- /**
38
- * @param {object} payload - PostToolUse stdin payload
39
- * @returns {Promise<null>}
40
- */
41
20
  export async function run(payload) {
42
21
  try {
43
22
  if (!SHELL_TOOLS.has(payload?.tool_name)) return null;
@@ -45,7 +24,7 @@ export async function run(payload) {
45
24
  const dir = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
25
  if (!stateExists(dir)) return null;
47
26
  kickGraphRefresh(dir);
48
- } catch { /* fail-open */ }
27
+ } catch {}
49
28
  return null;
50
29
  }
51
30
 
@@ -1,20 +1,7 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * heavy-gate.js — PreToolUse hook (Bash matcher), link of the Bash chain before worktree-env-inject.
4
- *
5
- * A sub-agent that runs `dotnet build` or `npm test` straight through Bash bypasses the heavy-node
6
- * semaphore that `morph-spec verify` respects. With `heavy.gateBash: true` this hook takes a slot
7
- * before such a command runs and `heavy-release` gives it back on Post/PostFailure, matched by
8
- * `tool_use_id`. It never rewrites the command: a rewrite would be judged against the user's
9
- * permission rules in its rewritten form, and would auto-approve what the prompt would have asked.
10
- *
11
- * A slot taken here has no live process to watch — the hook exits at once — so it lapses by TTL
12
- * (the call's own timeout plus slack). A call refused by a permission rule or interrupted by the
13
- * user fires no Post hook and keeps its slot until then.
14
- *
15
- * Waits below the platform's 600 s hook timeout, because a timed-out PreToolUse hook lets the call
16
- * run: the refusal has to come from here. Fail-open otherwise.
17
- */
2
+ // Never rewrites the command: a rewrite would be judged against the user's permission rules in its rewritten form.
3
+ // The lease has no live pid (the hook exits at once), so it lapses by TTL; a call that fires no Post hook holds it until then.
4
+ // The wait stays under the 600 s hook timeout because a timed-out PreToolUse hook lets the call run.
18
5
 
19
6
  import { stateExists } from '../../shared/state-reader.js';
20
7
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
@@ -22,11 +9,6 @@ import { heavyKind, leaseTtlMs } from '../../shared/heavy-command.js';
22
9
 
23
10
  const HOOK_BUDGET_SEC = 540;
24
11
 
25
- /**
26
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
27
- * @param {{ env?: object, acquire?: Function }} [deps] - test seams; production passes none
28
- * @returns {Promise<object|null>} a block result when no slot came free, else null (pass)
29
- */
30
12
  export async function run(payload, deps = {}) {
31
13
  try {
32
14
  if (payload?.tool_name && payload.tool_name !== 'Bash') return null;
@@ -1,21 +1,10 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * heavy-release.js — PostToolUse / PostToolUseFailure link: gives back the slot `heavy-gate` took
4
- * for this Bash call, matched by `tool_use_id`.
5
- *
6
- * Silent and fail-open. A call that fires neither event (refused by a rule, interrupted) leaves its
7
- * slot to the TTL. Releasing is not deciding, so this link is safe in the async failure binding.
8
- */
2
+ // A call that fires neither Post event leaves its slot to the TTL. Releasing is not deciding, so this link is safe async.
9
3
 
10
4
  import { existsSync } from 'node:fs';
11
5
  import { stateExists } from '../../shared/state-reader.js';
12
6
  import { runStandalone } from '../../shared/hook-response.js';
13
7
 
14
- /**
15
- * @param {object} payload - PostToolUse or PostToolUseFailure stdin payload
16
- * @param {{ env?: object }} [deps] - test seam
17
- * @returns {Promise<null>}
18
- */
19
8
  export async function run(payload, deps = {}) {
20
9
  try {
21
10
  if (payload?.tool_name !== 'Bash' || !payload.tool_use_id) return null;
@@ -27,7 +16,7 @@ export async function run(payload, deps = {}) {
27
16
  if (!existsSync(dir)) return null;
28
17
  const { releaseLeaseById } = await import('../../shared/heavy-slots.js');
29
18
  releaseLeaseById(dir, payload.tool_use_id);
30
- } catch { /* fail-open */ }
19
+ } catch {}
31
20
  return null;
32
21
  }
33
22
 
@@ -1,19 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: Loop Detection
5
- *
6
- * Event: PostToolUse | Matcher: none (all tools)
7
- *
8
- * Detects when an agent is stuck in a repetitive loop by tracking
9
- * the last 15 actions. If the last 8 actions have <= 2 unique
10
- * signatures, injects a warning with escape instructions.
11
- *
12
- * Cooldown: 5 minutes between warnings to prevent meta-loop.
13
- *
14
- * Fail-open: exits 0 on any error.
15
- */
16
-
17
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
18
4
  import { join } from 'path';
19
5
  import { stateExists } from '../../shared/state-reader.js';
@@ -24,13 +10,8 @@ import { logEvent } from '../../shared/activity-logger.js';
24
10
  const MAX_BUFFER = 15;
25
11
  const LOOP_WINDOW = 8;
26
12
  const MAX_UNIQUE_SIGS = 2;
27
- const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
13
+ const COOLDOWN_MS = 5 * 60 * 1000;
28
14
 
29
- /**
30
- * @param {object} payload - parsed PostToolUse stdin payload (any tool)
31
- * @returns {Promise<object|null>} a hookSpecificOutput loop warning, or null
32
- * (pass/silent).
33
- */
34
15
  export async function run(payload) {
35
16
  try {
36
17
  if (!stateExists()) return null;
@@ -43,7 +24,6 @@ export async function run(payload) {
43
24
 
44
25
  mkdirSync(memoryDir, { recursive: true });
45
26
 
46
- // Read existing buffer
47
27
  let buffer = [];
48
28
  try {
49
29
  if (existsSync(bufferPath)) {
@@ -54,19 +34,15 @@ export async function run(payload) {
54
34
  buffer = [];
55
35
  }
56
36
 
57
- // Append current action
58
37
  const sig = getCommandSignature(payload);
59
38
  buffer.push({ sig, ts: new Date().toISOString() });
60
39
 
61
- // Trim to max size (FIFO)
62
40
  if (buffer.length > MAX_BUFFER) {
63
41
  buffer = buffer.slice(-MAX_BUFFER);
64
42
  }
65
43
 
66
- // Save updated buffer
67
44
  writeFileSync(bufferPath, JSON.stringify(buffer), 'utf-8');
68
45
 
69
- // Check for loop (need at least LOOP_WINDOW actions)
70
46
  if (buffer.length < LOOP_WINDOW) return null;
71
47
 
72
48
  const recent = buffer.slice(-LOOP_WINDOW);
@@ -74,18 +50,14 @@ export async function run(payload) {
74
50
 
75
51
  if (uniqueSigs.size > MAX_UNIQUE_SIGS) return null;
76
52
 
77
- // Check cooldown
78
53
  try {
79
54
  if (existsSync(cooldownPath)) {
80
55
  const cooldown = JSON.parse(readFileSync(cooldownPath, 'utf-8'));
81
56
  const elapsed = Date.now() - new Date(cooldown.lastDetected).getTime();
82
57
  if (elapsed < COOLDOWN_MS) return null;
83
58
  }
84
- } catch {
85
- // Ignore corrupt cooldown — proceed with detection
86
- }
59
+ } catch {}
87
60
 
88
- // Loop detected — set cooldown and warn
89
61
  writeFileSync(cooldownPath, JSON.stringify({ lastDetected: new Date().toISOString() }), 'utf-8');
90
62
 
91
63
  logEvent('loop_detected', { signatures: uniqueSigs.size }, process.cwd());
@@ -102,7 +74,6 @@ export async function run(payload) {
102
74
  'PostToolUse'
103
75
  );
104
76
  } catch {
105
- // Fail-open
106
77
  return null;
107
78
  }
108
79
  }
@@ -38,10 +38,6 @@ export function matchesExecutableRule(command, rule) {
38
38
  return false;
39
39
  }
40
40
 
41
- /**
42
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
43
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
44
- */
45
41
  export async function run(payload) {
46
42
  try {
47
43
  if (!stateExists()) return null;
@@ -1,22 +1,5 @@
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/plan.md → after 'plan', only an append under `## Errata`
10
- * - 2-plan/checks.json → never by hand: `morph-spec plan-check` is its only writer
11
- * - 1-design/design-system.md, etc. → after 'uiux', only an errata append
12
- *
13
- * A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
14
- * exist on disk is a CREATION and passes — the criterion is the file's
15
- * existence, never who signed the gate (see the carve-out in run()).
16
- *
17
- * Fail-open: exits 0 on any error.
18
- */
19
-
20
3
  import { existsSync, readFileSync } from 'fs';
21
4
  import { stateExists, isGateApproved } from '../../shared/state-reader.js';
22
5
  import {
@@ -32,10 +15,6 @@ import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
32
15
 
33
16
  const CHECKS_FILE = 'checks.json';
34
17
 
35
- /**
36
- * Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
37
- * @returns {string|null} null quando o payload não permite reconstruir (fail-open).
38
- */
39
18
  function proposedText(payload, toolName, original) {
40
19
  if (toolName === 'Edit') {
41
20
  const oldStr = payload?.tool_input?.old_string;
@@ -49,14 +28,8 @@ function proposedText(payload, toolName, original) {
49
28
  return typeof content === 'string' ? content : null;
50
29
  }
51
30
 
52
- /**
53
- * @param {object} payload - parsed PreToolUse stdin payload
54
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
55
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
56
- */
57
31
  export async function run(payload) {
58
32
  try {
59
- // Amend mode: bypass spec protection for legitimate in-implementation corrections
60
33
  if (process.env.MORPH_AMEND_PHASE) {
61
34
  console.error(`[morph-spec] AMEND MODE: bypassing spec protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
62
35
  return null;
@@ -67,14 +40,9 @@ export async function run(payload) {
67
40
  const filePath = getFilePath(payload);
68
41
  if (!filePath) return null;
69
42
 
70
- // Only check files inside .morph/features/
71
43
  if (!isFeaturePath(filePath)) return null;
72
44
 
73
- // The gate that locks this file is recorded in the feature.json of the tree
74
- // that OWNS it. Asking the session's tree instead made this guard go DARK in
75
- // a secondary worktree: the root had no approved gate, so a post-Gate-2
76
- // plan.md rewrite sailed through. Same defect as enforce-phase-writes,
77
- // 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.
78
46
  const root = resolveMorphRoot(filePath, process.cwd());
79
47
  if (!stateExists(root)) return null;
80
48
 
@@ -84,12 +52,10 @@ export async function run(payload) {
84
52
  const filename = getBasename(filePath);
85
53
  const requiredGate = PROTECTED_SPEC_FILES[filename];
86
54
 
87
- // Not a protected file
88
55
  if (!requiredGate) return null;
89
56
 
90
- // Only plan-check writes checks.json; a hand-written one would carry
91
- // checks the parser never judged. Decided before the creation carve-out:
92
- // chore/hotfix are born with the plan gate stamped.
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.
93
59
  if (filename === CHECKS_FILE) {
94
60
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
95
61
  return blockResult(
@@ -100,53 +66,28 @@ export async function run(payload) {
100
66
  );
101
67
  }
102
68
 
103
- // Gate not yet approved — allow editing
104
69
  if (!isGateApproved(featureName, requiredGate, root)) {
105
70
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
106
71
  return null;
107
72
  }
108
73
 
109
- // ── Gate approved — file is locked ──────────────────────────────────────
110
74
  const absPath = resolveTargetPath(filePath, root);
111
75
 
112
- // Um gate aprovado congela o ARTEFATO APROVADO. Se o arquivo ainda não
113
- // existe no disco, não há artefato aprovado a proteger: isto é a CRIAÇÃO
114
- // inicial, não a edição de uma spec fechada. O caso é real e não é raro —
115
- // `chore`/`hotfix` nascem com `plan` carimbado `policy:{tipo}`
116
- // (create.js → work-types.js skippedGates), então o primeiro `Write` de
117
- // `2-plan/plan.md` já chega aqui com o gate "aprovado" sem que humano
118
- // nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
119
- // mandando revogar o gate de um arquivo inexistente.
120
- //
121
- // O critério é a EXISTÊNCIA, nunca o `approvedBy`. Liberar por assinatura
122
- // (`policy:` passa, `lucas` não) transformaria este carve-out em licença
123
- // para reescrever qualquer spec aprovada: bastaria apagá-la antes. Por ser
124
- // sobre o disco, a janela fecha sozinha no instante em que o arquivo
125
- // existe — os contra-canários de test/hooks/protect-spec-files.test.js
126
- // pinam exatamente isso.
127
- // O `result` distinto no log é o que torna a criaçã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.
128
78
  if (!existsSync(absPath)) {
129
79
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
130
80
  return null;
131
81
  }
132
82
 
133
- // ── Arquivo totalmente travado (plan.md, design) ────────────
134
- // O corpo continua congelado. A ÚNICA passagem é a errata append-only:
135
- // corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
136
- // de leitura/reconstrução cai no bloqueio: a regra base é "não se edita".
137
- //
138
- // O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
139
- // é a única coisa que este catch NÃO significa mais. ENOENT tratado como
140
- // "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.
141
84
  const toolName = getToolName(payload);
142
85
  let errataCheck = null;
143
86
  try {
144
87
  const original = readFileSync(absPath, 'utf-8');
145
88
  const proposed = proposedText(payload, toolName, original);
146
89
  if (proposed !== null) errataCheck = errataViolation(original, proposed);
147
- } catch {
148
- // arquivo ilegível → sem errata possível, segue para o bloqueio
149
- }
90
+ } catch {}
150
91
 
151
92
  if (errataCheck?.ok) {
152
93
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
@@ -165,7 +106,6 @@ export async function run(payload) {
165
106
  `edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
166
107
  );
167
108
  } catch {
168
- // Fail-open
169
109
  return null;
170
110
  }
171
111
  }