@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
@@ -9,6 +9,6 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent, AskUserQuestion, Skil
9
9
  Atalho: invoque `Skill(morph-apply)` com o nome da feature.
10
10
 
11
11
  Pré-requisito: o gate do plano aprovado, com `.morph/features/{feature}/2-plan/plan.md` e
12
- `2-plan/checks.json` (gravado por `morph-spec plan-check`). Faltando, rode `/morph-proposal {feature}`.
12
+ `2-plan/checks.json` (gravado por `morph-spec plan-check`). Faltando, rode `/morph-plan {feature}`.
13
13
 
14
14
  **Feature:** $ARGUMENTS
@@ -34,7 +34,7 @@ output completion (X/N outputs), slice progress (reports in `3-implement/reports
34
34
  After running the CLI, summarize:
35
35
  - Current phase and which gate (if any) is open
36
36
  - Outstanding outputs for the current phase
37
- - Recommended next slash command (`/morph-proposal`, `/morph-apply`, or a gate approval)
37
+ - Recommended next slash command (`/morph-plan`, `/morph-apply`, or a gate approval)
38
38
 
39
39
  ---
40
40
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  **Exceção `hotfix`:** esse tipo nunca despacha (`morph-hotfix` §7); o Gate 3 exige só `4-review/verifier.md`, escrito pelo orquestrador, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
18
18
 
19
- **No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
19
+ **No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela ou pelo protótipo** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
20
20
 
21
21
  **O hook state-sync.js** cobre o caminho manual: `AskUserQuestion` respondida com frase de aprovação (abaixo) → registra o gate no `feature.json` autoritativo com `approvedBy: "manual"`. **A feature é identificada pelo nome que a pergunta cita** — grava na árvore dona dela, nunca na "feature ativa" da sessão. Pergunta sem nome numa árvore com mais de uma feature: o hook **recusa gravar** e avisa — aprove por `morph-spec approve`.
22
22
 
@@ -38,11 +38,11 @@ No **review**, o `gate-check` lê os relatórios de `4-review/` (`src/lib/review
38
38
 
39
39
  Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
40
40
 
41
- **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") é **dispensa legítima** — passa, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. `e2eStatus`/`e2eReason`/`e2eSkipClass` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`.
41
+ **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `not-applicable` ("not applicable: …", a feature sem cenário `prova: e2e` e sem arquivo de UI, spec e2e ou container no diff) são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. `e2eStatus`/`e2eReason`/`e2eSkipClass` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`.
42
42
 
43
43
  **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma fatia do `checks.json` cujo `standards` cita `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` — pelo id de STANDARDS.json ou pelo caminho, com ou sem `framework/`/`standards/` na frente, porque uma grafia não prevista desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (`framework/evals/review-checklist.md`, "Feature que gera mídia").
44
44
 
45
- **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `verifier.md`, e a linha que o verificador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
45
+ **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". Com o checklist completo, cada tela ainda deve o **protótipo** (`prototypes`, só na uiux): a linha `**Protótipo:** 1-design/prototipos/<tela>.html` dentro da `## Tela:`, o HTML e os prints `<tela>-1280.png` e `<tela>-390.png` ao lado dele; faltando um, **pausa** nomeando a tela e o arquivo ("protótipo incompleto"), pelo mesmo predicado. No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `verifier.md`, e a linha que o verificador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
46
46
 
47
47
  **Por quê.** Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
48
48
 
@@ -58,10 +58,10 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
58
58
  | `verificationStatus` | === 'fail' | `verify` vermelho — build, testes, validadores **ou e2e**. Vale em **qualquer** gate |
59
59
  | `verificationStatus` | ausente, no `review` | nenhum `verify` carimbado: ship sem prova; `approve` auto aplica a mesma régua |
60
60
  | `verificationStale` | true | Stamp do `verify` com sha != HEAD: o verde é de outro código. Só no **ship** |
61
- | `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (só `project`). Só no **ship** |
61
+ | `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (`project` e `not-applicable` passam). Só no **ship** |
62
62
  | `promptModel` | prompt/modelo tocado | Sem nó `evals` declarado, ou eval pulado neste verify. Só no **ship** |
63
63
  | `mediaClaims` | seção ausente | Feature que gera mídia, relatório sem `## Mídia gerada`. Só no **ship** |
64
- | `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, ou nenhuma `## Tela:`. **ship**: item decidido sem linha em `## Checklist de tela` |
64
+ | `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo e prints 1280/390. **ship**: item decidido sem linha em `## Checklist de tela` |
65
65
  | `planChecks` | ≠ `fresh` | `checks.json` ausente ou de outra versão do `plan.md`: rode `plan-check`. Só no **plano**; `approve`/`advance` automáticos recusam |
66
66
  | `sliceCount` | > 12 | Plano grande demais para uma feature |
67
67
  | `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
@@ -268,7 +268,7 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
268
268
 
269
269
  ### claim
270
270
 
271
- **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
271
+ **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram o trabalho de uma fatia e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
272
272
 
273
273
  ## Validação
274
274
 
@@ -282,6 +282,8 @@ O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` d
282
282
 
283
283
  As specs são procuradas na raiz, em `project.frontendPath` e, sem ele, nas pastas de frontend conhecidas (as mesmas do detector, mais `web`). Se o nó cai para smoke enquanto existe um `playwright.config.*` não executado em pasta de primeiro nível, o `verify` avisa nomeando a pasta e `project.frontendPath`. A contagem de specs sai em `nodes.e2e.count` do `verify --json` (`null` quando não rodou ou o sumário é ilegível, nunca 0); ela não é persistida no stamp.
284
284
 
285
+ **O e2e só sobe quando pode provar algo:** com testes vermelhos ele não roda (`tests failed — the e2e does not run over a red suite`); sem nó de teste `e2e: true` (esses são a prova do backend contra a stack e sempre a sobem), ele também não roda quando nenhum cenário do `checks.json` é `prova: e2e` e o diff da feature contra a branch default (fora de `.morph/`) não toca UI (`.tsx`, `.css`, `.html`…), spec e2e, `playwright.config.*`, Dockerfile/compose nem pasta de frontend conhecida — `skip` com a razão `not applicable: …`, que o gate aceita. Sem git, sem base ou diff vazio: roda (só para diante de evidência).
286
+
285
287
  **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
286
288
 
287
289
  **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
@@ -316,7 +318,7 @@ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
316
318
 
317
319
  **Sem workflow** que rode `morph-spec verify` publicando `morph-verify.json`, cria e commita `.github/workflows/morph-verify.yml` na branch da feature (gatilho `push` em `morph/**`, `.morph/` fora do gatilho exceto o `config.json`, `cancel-in-progress`, setup de .NET por `global.json`/stack, install por lockfile, browsers do Playwright onde há config, e o `morph-spec` na versão de `.morph/.morphversion`). Um run sobre um commit anterior vale quando só `.morph/` andou desde ele; run `cancelled`/`skipped`/`startup_failure` não é prova.
318
320
 
319
- **Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de rodar local, reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não roda nem regrava, imprime "stamp fresco reaproveitado" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
321
+ **Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de tudo — com ou sem CI — reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não empurra, não roda nem regrava, imprime "stamp verde e fresco — o CI não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
320
322
 
321
323
  Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com task, `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
322
324
 
@@ -328,25 +330,25 @@ Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rod
328
330
 
329
331
  **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`).
330
332
 
331
- Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1,5×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário).
333
+ Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário).
332
334
 
333
335
  Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas.
334
336
 
335
- Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro. Fica fora do modo `light`.
337
+ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro.
336
338
 
337
339
  **Por quê.** Excesso justificado não silencia o aviso: a justificativa mora na task (orçamento do `morph-plan` passo 5), e é lá que o avaliador a lê.
338
340
 
339
341
  ### mutate
340
342
 
341
- **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--no-subset] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o verificador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um cenário exige e o verificador não achou o teste que a morde.
343
+ **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o verificador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um cenário exige e o verificador não achou o teste que a morde.
342
344
 
343
345
  **Manifesto** (`--plan`, default `4-review/mutants.json`): `{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a","expect":"OrdersTests.RecusaSemItens deve falhar"}]}` — `expect` é obrigatório (qual teste deve falhar e por quê, dito antes de rodar), `find` começa na `line` e é único nela; barra invertida do código vai como `{{BS}}` (o transporte do Bash a reduz). Mais de 3 mutantes ou `expect` ausente: exit 2.
344
346
 
345
347
  **O que compila (.NET):** o `build` do nó quando declarado; senão o que o comando nomeia (`.sln`/`.slnx`/`.csproj` — a solution vale pelos projetos que lista, também para o subconjunto); senão a solution única (ou o projeto único) do diretório alvo, como o próprio `dotnet test` escolhe. Só sem nada descobrível recusa (exit 2), antes de mutar, pedindo a solution no comando ou o `build`.
346
348
 
347
- **Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o verificador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado; sem subconjunto utilizável, a suíte inteira roda e a morte tem de se repetir.
349
+ **Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o verificador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado; rodada que cai é `inconclusive`. **Nunca a suíte inteira:** sem subconjunto utilizável (nenhum teste da feature nem citando os tipos mutados) o comando recusa antes de compilar (exit 2) e manda provar o cenário por leitura; subconjunto que some em runtime (base que não seleciona teste, ou cai) deixa os mutantes `inconclusive` com a mesma razão.
348
350
 
349
- **Nó `e2e: true`:** arquivo coberto **só** por nó de teste `e2e: true` é recusado (exit 2, nomeando mutante, arquivo e nó): a suíte testa o container do compose e o `mutate` recompila só localmente — todo mutante sobreviveria em falso. Com outro nó dono do arquivo, ele é o escolhido — mas, havendo qualquer nó `e2e: true` no config, só se algum arquivo de teste dele (com atributo de teste) citar um tipo declarado no arquivo mutado: o dono de um `.cs` é a suíte .NET, não a referência de projeto, e a unidade que só referencia a API daria sobrevivente em falso. Sem citação, recusa (exit 2) antes do snapshot. Nas duas recusas a saída prescreve o protocolo manual com o comando pronto do nó e2e, cada passo numa linha só encadeado por `&&` (nunca `;`): base verde antes do 1º mutante (`eval "$(morph-spec env <f>)" && morph-spec e2e up <f> && morph-spec heavy run -- <comando do nó>`), a mesma cadeia depois de cada edição, e restauração conferida por `git diff --quiet` seguida de um novo `e2e up` — registrado em `4-review`. Sem nó `e2e: true`, o sobrevivente é achado real e roda como antes.
351
+ **Nó `e2e: true`:** arquivo coberto **só** por nó de teste `e2e: true` é recusado (exit 2, nomeando mutante, arquivo e nó): a suíte testa o container do compose e o `mutate` recompila só localmente — todo mutante sobreviveria em falso. Com outro nó dono do arquivo, ele é o escolhido — mas, havendo qualquer nó `e2e: true` no config, só se algum arquivo de teste dele (com atributo de teste) citar um tipo declarado no arquivo mutado: o dono de um `.cs` é a suíte .NET, não a referência de projeto, e a unidade que só referencia a API daria sobrevivente em falso. Sem citação, recusa (exit 2) antes do snapshot. Nas duas recusas a saída prescreve o protocolo manual com o comando pronto do nó e2e, cada passo numa linha só encadeado por `&&` (nunca `;`): base verde antes do 1º mutante (`eval "$(morph-spec env <f>)" && morph-spec e2e up <f> && morph-spec heavy run -- <comando do nó>`), a mesma cadeia depois de cada edição, e restauração conferida por `git diff --quiet` seguida de um novo `e2e up` — registrado em `4-review`. Sem nó `e2e: true`, vale a regra do subconjunto acima.
350
352
 
351
353
  **Sem `<feature>`:** vale a única feature em andamento; sem ela (nenhuma, ou mais de uma), exige `--plan`, e a seção só é gravada com `--out` — sai no stdout, `section: null` no `--json`.
352
354
 
@@ -496,6 +498,10 @@ Sem fatia na linha de comando, a saída traz `reviews: { verifier, reviewer, sec
496
498
 
497
499
  **`morph-spec stack-scan [<feature>] [--json]`** — varredura determinística do código contra `config.project.stack`: só os stacks configurados são julgados (sem `project.stack`, nenhuma regra roda e o resultado é `não auditado: project.stack ausente em .morph/config/config.json`, nunca "Em compliance" — exit 0; stack sem regras de varredura sai `não auditado` pelo mesmo motivo; código de um stack não configurado é só anotado). `dotnet`: TargetFramework abaixo de net10 e ORM/banco diferente (Dapper puro, SQL Server, MySQL) são **Bloqueante**; `Guid.NewGuid()`, repository genérico (`IRepository<T>`/`IUnitOfWork` — código morto: apagar a pasta `Repository/` e as 2 linhas de DI), classe `*Service`/`*Manager` sobre `DbContext` e SQL cru (`FromSqlRaw`, `ExecuteSqlRaw`, `SqlQueryRaw`, `ExecuteQuery`) são **Alto**; classe não-sealed é **Médio**; pacote `Microsoft.EntityFrameworkCore|AspNetCore|Extensions` abaixo da major 10 é **Baixo**. `nextjs`: frontend sem Next.js é Bloqueante; Pages Router sem `app/` e biblioteca de UI diferente (MUI, antd, Chakra, Bootstrap, Mantine) são Médio. Código de teste não conta; `node_modules`, `bin`, `obj`, `.morph`, `.claude` e os contêineres de worktree ficam fora; arquivo ilegível é anotado, nunca derruba. Sem código fonte = GREENFIELD. As contagens são de texto e superestimam consumidores — o relatório aponta `graph affected <símbolo> --uses-only`. Desvio Bloqueante/Alto vira nó da árvore de decisões da `morph-plan`. Com `<feature>`, grava `.morph/features/<f>/2-plan/stack-scan.md` (exit 2 se a feature não existe, 1 fora da árvore dona); sem, imprime o markdown.
498
500
 
501
+ ### tests-audit
502
+
503
+ **`morph-spec tests-audit [--json]`** — auditoria **somente leitura** dos arquivos de teste rastreados (`git ls-files` + `isTestPath`): total de casos (`[Fact]`/`[Theory]`, `it`/`test`), os maiores arquivos e quatro listas de **candidatos** — nunca vereditos, nada é apagado: teste que lê `.md`/`.txt` ou tem 3+ asserts de frase literal (fixa texto, não comportamento); constante comparada ao próprio literal; família de 4+ `[Fact]` com o mesmo prefixo de nome (candidata a `[Theory]`); o mesmo assunto em mais de uma camada (`XHandlerTests` e `XEndpointTests`/`XIntegrationTests` — fique com a mais barata que morde). Fora de git: exit 2 com a razão. É o ponto de partida de uma limpeza: apagar teste e depois rodar `verify --record-baseline` grava a contagem menor.
504
+
499
505
  ## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
500
506
 
501
507
  O nó `tests` do `verify` é **plural**: um projeto pode declarar quantas suítes tiver em
@@ -677,7 +683,7 @@ sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógi
677
683
  **O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
678
684
  escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
679
685
  atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
680
- vermelha, execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
686
+ vermelha (fora o `count-drop` com exit 0, abaixo), execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
681
687
  afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
682
688
  `project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
683
689
  fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
@@ -685,11 +691,7 @@ recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A g
685
691
  pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
686
692
  testes rodaram. A execução que **gravou** a baseline (verde e inteira) persiste o stamp `verification` como qualquer verify: ela mediu a suíte toda, e rodá-la de novo só para ter prova seria pagar duas vezes pelo mesmo
687
693
  verde. Sem gravação (vermelha, parcial, contagem desconhecida) não há stamp, e o `reason` do `--json`
688
- nomeia o nó que impediu. **Consequência a conhecer:** um `expectedCount` digitado à
689
- mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
690
- `--record-baseline` recusa — preservando o 9. **Não há flag de regravação forçada:** rebaixe ou apague
691
- o número à mão e então re-meça pelo comando. O caso oposto se conserta sozinho — número baixo demais
692
- passa com aviso de alta, e o comando grava.
694
+ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha que a gravação aceita é o `count-drop` de um nó que saiu com exit 0 — a suíte ficou menor de propósito. `--record-baseline` grava o número menor que mediu, o nó fica verde nessa mesma execução, o stamp persiste e o `feedback` diz `[baseline] rebaixada: <nó> (9 → 7)`; o revisor do Gate 3 vê a queda e julga se foi limpeza. Sem a flag, a queda continua reprovando (regressão silenciosa). Queda ao lado de outro nó vermelho não grava nada.
693
695
 
694
696
  **A divisão loop × suíte.** É a resposta ao "a suíte inteira não cabe numa chamada":
695
697
 
@@ -735,7 +737,7 @@ Falhas do `--show` saem 1: id inexistente (`Standard "<id>" não existe.` com at
735
737
  **`--dry-run` prevê sem tocar em nada.** Previsão e execução são o **mesmo percurso**, com os **mesmos filtros** e o **mesmo coletor de efeitos**: em modo normal cada verbo executa *e* registra, em dry apenas registra.
736
738
 
737
739
  O relatório agrupa por natureza do efeito (apagar, criar diretório, copiar, escrever, mesclar, executar, índice do git) e **nomeia separadamente três grupos que ninguém adivinha da palavra "update"**:
738
- - **apagados por inteiro antes da reinstalação** — `.claude/{commands,rules,skills,agents}` e `.morph/{framework/standards,framework/hooks,framework/schemas,framework/rubrics,framework/evals,config}`, com a contagem de arquivos que hoje moram em cada um (`framework/rubrics` não é mais instalado: entra na lista só para sumir de instalações antigas);
740
+ - **apagados por inteiro antes da reinstalação** — `.claude/{commands,rules,skills,agents}` e `.morph/{framework/standards,framework/hooks,framework/schemas,framework/evals,config}`, com a contagem de arquivos que hoje moram em cada um;
739
741
  - **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
740
742
  - **dentro do pacote da CLI** — `generate-refs.js` escrevendo `framework/hooks/shared/phase-utils.js` do pacote, que num `npm link` de desenvolvimento é o próprio repositório do framework. Em dry roda a variante `--check`, que verifica e não escreve.
741
743
 
@@ -743,17 +745,6 @@ O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bl
743
745
 
744
746
  **Freio de major.** Um salto de **dois majors ou mais** é recusado sem `--force`, **antes de qualquer mutação** — recusar depois de mutar é meio-estado. A versão instalada vem de `.morph/.morphversion → morphVersion`, o mesmo registro que o `update` já lê; **nunca** de `config.json → frameworkVersion`, que o `update` escreve e jamais lê para decidir (medido: os dois divergem por um major inteiro num projeto real). Quando os dois discordam, a mensagem imprime **os dois** e diz qual decidiu — escolher em silêncio é acertar metade das vezes e nunca saber qual metade. `.morphversion` ausente ou ilegível é tratado como salto grande, com motivo próprio (`versão instalada indeterminável`) distinto do salto medido: "não sei quão velho isto é" é precisamente quando sobrescrever às cegas é mais perigoso. `--force` dispensa o freio e o relatório final **registra a dispensa**. `--dry-run` combinado com uma recusa **imprime o relatório**, com a recusa como primeiro item.
745
747
 
746
- **Feature em voo no formato anterior.** Antes de qualquer mutação, o `update` varre `.morph/features/` da raiz e de cada worktree que o `git worktree list` conhece; uma feature não encerrada (`status` diferente de `completed`/`abandoned`) com `2-plan/tasks.json` ou `0-proposal/` e sem `2-plan/plan.md` é do formato que esta versão não conduz, e o `update` recusa (exit 1, nada escrito):
747
-
748
- ```
749
- Recusado: N feature(s) em voo no formato anterior ao plan.md (proposal/spec/tasks.json), que esta versão não conduz:
750
- - <feature> (raiz | <caminho da worktree>)
751
- Termine cada uma na 8.47: `npx @polymorphism-tech/morph-spec@8.47 <comando>` (por exemplo `finish <feature> --pr`),
752
- ou arquive sem integrar: `morph-spec archive <feature> --force` (o Gate 3 dela não foi aprovado). Depois rode o `update` de novo.
753
- ```
754
-
755
- `--force` **não** dispensa esta recusa (o freio de major, sim): a CLI nova não sabe conduzir essas features. Feature em `.morph/archive/` não conta. `--dry-run` põe as mesmas linhas no topo da previsão. A mesma regra roda no `init --force` e no `setup-infra` (inclusive `--reinstall`), que sobrescrevem `.morph/` e `.claude/` sem passar pelo `update`: recusam com as mesmas linhas e saem 1. No retorno programático, `{ refused: true, reason: 'old-format-features', features }`.
756
-
757
748
  **Por quê.** Não há travessia paralela — o resumo de uma execução real também sai do coletor, de modo que um ponto de emissão esquecido some das duas saídas ao mesmo tempo, em vez de mentir só no `--dry-run`.
758
749
 
759
750
  ## Grafo de codebase
@@ -810,7 +801,7 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
810
801
 
811
802
  ### cost
812
803
 
813
- **`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
804
+ **`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`checklist`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
814
805
 
815
806
  ### cost --metrics
816
807
 
@@ -41,7 +41,10 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
41
41
  ## Verificador (`4-review/verifier.md`)
42
42
 
43
43
  Missão: **provar cada cenário** do `2-plan/checks.json`. O default de todo `Cn` é NÃO PROVADO;
44
- critério que você não verificou nunca vira PROVADO por inferência.
44
+ critério que você não verificou nunca vira PROVADO por inferência. A prova é a que o plano declarou
45
+ (`prova:` do Cn): `leitura` prova-se pelo `arquivo:linha` e pela fiação, **sem teste** — Cn `leitura`
46
+ sem teste não é NÃO PROVADO, e pedir teste para ele não é bloqueador. Só Cn com `risco:` exige teste
47
+ que morde.
45
48
 
46
49
  ```markdown
47
50
  ## Checks
@@ -82,7 +85,9 @@ morde. Escreva `4-review/mutants.json` e rode `morph-spec mutate {feature}` **um
82
85
 
83
86
  `expect` é obrigatório: diga qual teste deve falhar e por quê **antes** de rodar — mutante cujo
84
87
  efeito você não sabe prever tende a ser equivalente. Barra invertida do código vira `{{BS}}`.
85
- Sobreviveu numa linha que um cenário exige → bloqueador `teste`. Sobreviveu em outra → nota. Cole a
88
+ Sobreviveu numa linha que um cenário exige → bloqueador `teste`. Sobreviveu em outra → nota.
89
+ Sem subconjunto (exit 2) ou `inconclusive`: a mutação não roda a suíte inteira — prove por leitura
90
+ (`arquivo:linha` + o teste que cobre), nunca em laço. Cole a
86
91
  seção `## Mutações` que a ferramenta imprime. A reversão é da ferramenta; rodada morta no meio:
87
92
  `morph-spec mutate --restore`.
88
93
 
@@ -109,6 +114,11 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
109
114
  árvore de trabalho — o verificador pode estar com um mutante aplicado nela. Default SHIP.
110
115
 
111
116
  1. **Escopo**: o diff contra as fatias (`arquivos:`) e o `## Contrato de implementação` do plano.
117
+ Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
118
+ risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
119
+ camadas — bloqueador `escopo` (apague, não acrescente). `expectedCount` menor no
120
+ `.morph/config/config.json` do diff sem teste apagado no mesmo diff é bloqueador `regressao`: é
121
+ suíte que parou de rodar, não limpeza.
112
122
  2. **Arquitetura**: padrão ou abstração nova contra o stack do projeto e os standards (camada que o
113
123
  projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano).
114
124
  3. **Regressão**: para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`.
@@ -6,41 +6,16 @@ Comprehensive hooks system for enforcing spec-driven development at the Claude C
6
6
 
7
7
  ```
8
8
  framework/hooks/
9
- ├── claude-code/ # Claude Code native hooks
10
- │ ├── core/
11
- │ │ ├── worktree-context.js # SessionStart: worktree feature identity / active-feature summary
12
- │ │ ├── worktree-create.js # WorktreeCreate: governed tree via `morph-spec worktree setup|provision`
13
- │ │ ├── worktree-remove.js # WorktreeRemove: safe teardown via `worktree remove --if-clean`
14
- │ │ ├── block-destructive-worktree-removal.js # PreToolUse(Bash): raw removal follows junctions out
15
- │ │ ├── block-raw-worktree-add.js # PreToolUse(Bash): raw `git worktree add` is ungoverned
16
- │ │ └── warn-bash-source-write.js # PreToolUse(Bash): source written by redirect skips the Write/Edit guards
17
- │ ├── user-prompt/
18
- │ │ └── enrich-prompt.js # Context-aware prompt enrichment
19
- │ ├── pre-tool-use/
20
- │ │ ├── protect-spec-files.js # Block edits to approved spec artifacts
21
- │ │ └── enforce-phase-writes.js # Enforce writes to correct phase dir
22
- │ ├── post-tool-use/
23
- │ │ └── dispatch.js # Dispatch on CLI commands (auto-checkpoint)
24
- │ ├── stop/
25
- │ │ └── validate-completion.js # Advisory: warn about incomplete work
26
- │ ├── pre-compact/
27
- │ │ └── save-morph-context.js # Snapshot state before compaction
28
- ├── shared/ # Reusable utilities for all hooks
29
- │ ├── state-reader.js # Read-only state.json accessor
30
- │ ├── phase-utils.js # Phase constants and path utilities
31
- │ ├── hook-response.js # JSON response builders
32
- │ └── stdin-reader.js # Stdin JSON reader
33
- ├── git/ # Git hooks (Bash)
34
- │ ├── pre-commit/
35
- │ │ ├── orchestrator.sh # Master hook dispatcher
36
- │ │ └── agents.sh # Validates agents.json schema
37
- │ ├── commit-msg/
38
- │ │ └── conventional-commits.sh # Enforces conventional commits
39
- │ └── pre-push/
40
- │ └── run-tests.sh # Runs test suite before push
41
- └── README.md # This file
9
+ ├── claude-code/
10
+ │ ├── core/ # every hook, plus chain.js (one process per multi-hook group)
11
+ │ └── statusline.py # global statusline
12
+ ├── shared/ # code hooks share (and src/ imports): state reader, guards, e2e, heavy, telemetry
13
+ └── README.md
42
14
  ```
43
15
 
16
+ The registered set is `MORPH_HOOKS` in `src/utils/hooks-installer.js` — the table below lists it. No hook builds,
17
+ typechecks or runs tests: that is `morph-spec verify`.
18
+
44
19
  ## Hook Events
45
20
 
46
21
  | Event | Hook | Type | Purpose |
@@ -104,17 +79,6 @@ The installer writes to `.claude/settings.local.json`:
104
79
  }
105
80
  ```
106
81
 
107
- ### Git Hooks
108
-
109
- ```bash
110
- # In your project root
111
- cd .git/hooks
112
- ln -sf ../../framework/hooks/git/pre-commit/orchestrator.sh pre-commit
113
- ln -sf ../../framework/hooks/git/commit-msg/conventional-commits.sh commit-msg
114
- ln -sf ../../framework/hooks/git/pre-push/run-tests.sh pre-push
115
- chmod +x pre-commit commit-msg pre-push
116
- ```
117
-
118
82
  ## Shared Utilities
119
83
 
120
84
  All Claude Code hooks import from `framework/hooks/shared/`:
@@ -6,70 +6,31 @@ import { stateExists } from '../../shared/state-reader.js';
6
6
  import { scanForBlockers } from '../../shared/reparse-scan.js';
7
7
  import { tokenize } from '../../shared/command-tokens.js';
8
8
 
9
- /**
10
- * block-destructive-worktree-removal — the only defense that does not depend on
11
- * someone reading documentation.
12
- *
13
- * A morph worktree carries JUNCTIONS to the primary root's shared infra
14
- * (`node_modules`, `.claude`, `.morph/framework`). Every generic recursive
15
- * removal — `git worktree remove` (plain AND `--force`), `rm -rf`,
16
- * `Remove-Item -Recurse`, `rmdir /s` — FOLLOWS those links out of the worktree
17
- * and deletes what they point at, leaving the primary root's `.claude/` and
18
- * `.morph/framework/` as empty directories. That is the incident this hook
19
- * exists for, and it is not hypothetical: it happened, and the command was
20
- * copied from a message the CLI itself printed.
21
- *
22
- * `morph-spec worktree remove` is safe because it drops the junctions BEFORE
23
- * letting git near the directory. This hook is what stops the unsafe path from
24
- * being reachable by accident.
25
- *
26
- * DELIBERATELY NARROW — an inert or over-eager guardrail is worse than none:
27
- * - only recursive-removal shapes, only when a target path can be extracted;
28
- * - only when that path exists AND the scan finds reparse points under it
29
- * (an already-unlinked worktree, or one that never had junctions, passes);
30
- * - anything unparseable passes.
31
- * The scan is the authority, not the path shape: a worktree kept outside
32
- * `worktrees/` is protected just the same, and a `worktrees/` directory with
33
- * nothing to traverse is not blocked for looking dangerous.
34
- */
9
+ // Generic recursive removals FOLLOW a worktree's junctions and empty the root's `.claude/` and `.morph/framework/`.
10
+ // The reparse scan is the authority, not the path shape; anything unparseable passes.
35
11
 
36
- // The tokenizer lives in shared/command-tokens.js — block-raw-worktree-add.js
37
- // reads `git worktree add` off the same tokens, and a hook must never import a
38
- // sibling hook (its standalone tail would fire in the importer's process).
12
+ // A hook never imports a sibling hook (its standalone tail would fire), so the tokenizer lives in shared/.
39
13
  export { tokenize };
40
14
 
41
- /**
42
- * The path a recursive-removal command would destroy, or null when this is not
43
- * such a command (or the target cannot be read off it).
44
- *
45
- * @param {string} command
46
- * @returns {string|null}
47
- */
48
15
  export function extractRemovalTarget(command) {
49
16
  const tokens = tokenize(command);
50
17
  if (!tokens.length) return null;
51
18
  const lower = tokens.map((t) => t.toLowerCase());
52
19
 
53
- // `git worktree remove [--force] <path>` — the path is always the last
54
- // non-flag token; git has no form of this command without one.
20
+ // The path is always the last non-flag token.
55
21
  const gi = lower.indexOf('git');
56
22
  if (gi !== -1 && lower[gi + 1] === 'worktree' && lower[gi + 2] === 'remove') {
57
23
  const rest = tokens.slice(gi + 3).filter((t) => !t.startsWith('-'));
58
24
  return rest.length ? rest[rest.length - 1] : null;
59
25
  }
60
26
 
61
- // Generic recursive deletes. Each needs BOTH its command word and a recursive
62
- // flag — a plain `rm file` or `Remove-Item file` cannot traverse a junction's
63
- // contents and is none of this hook's business.
27
+ // Needs both the command word and a recursive flag: a plain `rm file` cannot traverse a junction.
64
28
  const recursive = tokens.some((t) => /^(-[a-z]*r[a-z]*|--recursive|\/s)$/i.test(t));
65
29
  const removalCmd = lower.find((t) => /^(rm|remove-item|ri|rd|rmdir|del)$/.test(t));
66
30
  if (!removalCmd || !recursive) return null;
67
31
 
68
32
  const idx = lower.indexOf(removalCmd);
69
- // A cmd.exe switch is a slash plus a letter or two (`rmdir /s /q`). Anything
70
- // longer that starts with `/` is a POSIX ABSOLUTE PATH — filtering on the
71
- // bare `/` made every Linux target look like a flag, so `rm -rf /tmp/wt`
72
- // extracted nothing and the hook passed exactly where it had to block.
33
+ // A cmd.exe switch is a slash plus one or two letters; anything longer is a POSIX absolute path.
73
34
  const isCmdSwitch = (t) => /^\/[a-z]{1,2}$/i.test(t);
74
35
  const operands = tokens.slice(idx + 1).filter((t) => !t.startsWith('-') && !isCmdSwitch(t));
75
36
  // Named-parameter form: `Remove-Item -Path <p> -Recurse`.
@@ -78,10 +39,6 @@ export function extractRemovalTarget(command) {
78
39
  return operands.length ? operands[0] : null;
79
40
  }
80
41
 
81
- /**
82
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
83
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
84
- */
85
42
  export async function run(payload) {
86
43
  try {
87
44
  if (!stateExists()) return null;
@@ -5,56 +5,23 @@ import { stateExists } from '../../shared/state-reader.js';
5
5
  // standalone tail (stdin read + exit) inside THIS process.
6
6
  import { tokenize } from '../../shared/command-tokens.js';
7
7
 
8
- /**
9
- * block-raw-worktree-add — PreToolUse (Bash) guard against `git worktree add`.
10
- *
11
- * A worktree created by raw git is a worktree morph cannot see: no infra
12
- * junctions (`.claude`, `node_modules`, `.morph/framework` — so NO hooks run
13
- * in there), no port block (its dev server collides with the root's), no
14
- * feature/task identity (invisible to `fleet`, `doctor --worktrees`,
15
- * `worktree list`), and a location git chose, not `<root>/worktrees/`. The
16
- * sibling guard (block-destructive-worktree-removal) protects the way OUT;
17
- * this one protects the way IN, by naming the two commands that build a
18
- * governed tree:
19
- *
20
- * morph-spec worktree setup <feature> — a feature's tree
21
- * morph-spec worktree provision <name> [--base <ref>] — a task-isolation tree
22
- *
23
- * DELIBERATELY NARROW: only the `add` subcommand. `list`, `prune`, `remove`
24
- * (the sibling's business), `lock`, `unlock`, `move`, `repair` all pass. The
25
- * `git` token may be followed by global options (`-C <dir>`, `--git-dir=<d>`,
26
- * `-c k=v`) before `worktree`, and may sit anywhere in a chained command
27
- * (`cd a && git worktree add b`) — every `git` occurrence is checked.
28
- */
29
-
30
- /** Global `git` options that take a separate value: `git -C <dir> worktree add`. */
31
8
  const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
32
9
 
33
- /**
34
- * Does this command line invoke `git worktree add` anywhere?
35
- * @param {string} command
36
- * @returns {boolean}
37
- */
38
10
  export function isRawWorktreeAdd(command) {
39
11
  const tokens = tokenize(command);
40
12
  const lower = tokens.map((t) => t.toLowerCase());
41
13
  for (let i = 0; i < lower.length; i++) {
42
14
  if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
43
15
  let j = i + 1;
44
- // Skip global options between `git` and the subcommand.
45
16
  while (j < lower.length && lower[j].startsWith('-')) {
46
- if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2; // `-C dir` consumes its value
47
- else j += 1; // `--git-dir=x`, `--no-pager`, …
17
+ if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2;
18
+ else j += 1;
48
19
  }
49
20
  if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
50
21
  }
51
22
  return false;
52
23
  }
53
24
 
54
- /**
55
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
56
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
57
- */
58
25
  export async function run(payload) {
59
26
  try {
60
27
  if (!stateExists()) return null;
@@ -1,54 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * chain.js — roda vários hooks core num único processo node.
4
- *
5
- * Motivação (onda de performance R2): cada hook registrado no Claude Code é
6
- * hoje um processo node próprio (~60ms de cold start). Um único Write/Edit
7
- * disparava até 9 desses processos — 2 PreToolUse + 3+2+2 PostToolUse. Este
8
- * script agrupa os elos de um mesmo event+matcher (a mesma entrada do
9
- * registry em src/utils/hooks-installer.js) num só spawn: lê stdin UMA vez,
10
- * importa e chama `run(payload)` de cada hook em sequência, e combina as
11
- * respostas — reduzindo esses 9 spawns para 4 por Write/Edit (1 PreToolUse +
12
- * 3 PostToolUse: Write|Edit / Write|Edit|AskUserQuestion / matcher-`*`).
13
- *
14
- * Uso: `node chain.js <hook1> <hook2> ...` — nomes de arquivo SEM `.js`,
15
- * resolvidos no mesmo diretório deste script (core/), na ORDEM em que devem
16
- * rodar. A ordem é a mesma que o registry antigo (duas entries separadas)
17
- * já implicava — ver o comentário de cada grupo em hooks-installer.js.
18
- *
19
- * Cada hook core exporta `run(payload)` (ver framework/hooks/shared/hook-response.js
20
- * `runStandalone`) — o arquivo continua executável standalone (spawnSync
21
- * direto, como os testes já fazem) porque a própria cauda do arquivo só lê
22
- * stdin/imprime/sai quando NÃO está sendo importado por esta cadeia.
23
- *
24
- * Regras de combinação:
25
- * - PreToolUse (decision:'block'): o PRIMEIRO elo que retornar um bloqueio
26
- * VENCE e encerra a cadeia — os elos seguintes nem chegam a rodar. Isto é
27
- * uma otimização NOVA desta consolidação (antes, Claude Code podia
28
- * disparar os dois processos de qualquer forma); o resultado observável
29
- * pro Claude Code é idêntico (o tool call é bloqueado do mesmo jeito).
30
- * - PostToolUse / outros eventos de contexto: os textos de TODOS os elos que
31
- * retornarem `hookSpecificOutput.additionalContext` (ou `decision:'approve'`
32
- * com additionalContext) são concatenados, na ORDEM dos argv, separados por
33
- * uma linha em branco — nenhum elo é pulado por um vizinho ter respondido.
34
- * - Nome do evento (hookSpecificOutput.hookEventName) no resultado final:
35
- * o valor que o PRIMEIRO elo a declarar um venceu (mesma regra de
36
- * "primeiro vence" do bloqueio acima — na prática todos os elos de um
37
- * mesmo grupo observam o MESMO evento real do Claude Code, então isto só
38
- * importa como desempate determinístico); na ausência de qualquer elo
39
- * declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
40
- * - updatedInput (PreToolUse): um elo pode devolver
41
- * `hookSpecificOutput.updatedInput` (ver `updatedInputResult` em
42
- * hook-response.js) para reescrever o tool_input. O ÚLTIMO elo a devolver um
43
- * vence (só um elo do grupo reescreve, por construção). `permissionDecision:
44
- * 'allow'` + `updatedInput` só entram no resultado final quando algum elo
45
- * reescreveu — um `allow` incondicional pularia o prompt de permissão de
46
- * TODO Bash, que é exatamente o contrário de um guardrail.
47
- * - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
48
- * falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
49
- * ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
50
- * try/catch; isto é a segunda rede de segurança, para o import em si.
51
- */
2
+ // Usage: node chain.js <hook> ... (names without .js, run in argv order). The first block wins and ends the chain,
3
+ // the contexts of every link concatenate, and an exception in one link never stops the next.
52
4
 
53
5
  import { dirname, join } from 'path';
54
6
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -56,20 +8,12 @@ import { readStdin } from '../../shared/stdin-reader.js';
56
8
 
57
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
58
10
 
59
- // Marca "já estou dentro de uma cadeia" para os hooks importados abaixo: a
60
- // cauda de execução standalone de cada hook (runStandalone() em
61
- // hook-response.js) só dispara quando esta flag NÃO está setada. Setada
62
- // ANTES do primeiro import dinâmico — cada hook importado herda o mesmo
63
- // process.env deste processo.
11
+ // Set before the first import: each hook's standalone tail runs only when this is unset.
64
12
  process.env.MORPH_HOOK_CHAINED = '1';
65
13
 
66
- // Override só para testes da mecânica genérica desta cadeia (fixtures fora
67
- // de core/, sem precisar ser hooks reais nem satisfazer as convenções que
68
- // hook-wiring.test.js exige de todo arquivo em core/). Produção nunca seta
69
- // isto — resolve sempre no próprio diretório deste script.
14
+ // Test-only override; production always resolves next to this script.
70
15
  const coreDir = process.env.MORPH_HOOK_CORE_DIR || __dirname;
71
16
 
72
- /** Extrai o texto de contexto de um resultado de hook, se houver. */
73
17
  function contextOf(result) {
74
18
  if (!result) return null;
75
19
  if (result.decision === 'approve' && typeof result.additionalContext === 'string') {
@@ -93,13 +37,11 @@ async function main() {
93
37
  const mod = await import(pathToFileURL(join(coreDir, `${name}.js`)).href);
94
38
  result = await mod.run(payload);
95
39
  } catch {
96
- // Fail-open POR ELO — ver doc do módulo acima.
97
40
  result = null;
98
41
  }
99
42
 
100
43
  if (!result) continue;
101
44
 
102
- // PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
103
45
  if (result.decision === 'block') {
104
46
  // A later link blocking must not swallow what earlier links reported (a tsc error on the same edit).
105
47
  if (contexts.length) result = { ...result, reason: [...contexts, result.reason].join('\n\n') };
@@ -116,9 +58,6 @@ async function main() {
116
58
  const ctx = contextOf(result);
117
59
  if (ctx) {
118
60
  contexts.push(ctx);
119
- // First link to declare a name wins (mirrors block-first-wins above) —
120
- // in practice every link in a group observes the SAME real Claude Code
121
- // event, so this only matters as a deterministic tie-break.
122
61
  eventName = eventName || result?.hookSpecificOutput?.hookEventName;
123
62
  }
124
63
  }
@@ -1,17 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: comments the edit added in a shape the
5
- * `architecture-code-comments` standard never allows.
6
- *
7
- * Event: PostToolUse | Matcher: Write|Edit
8
- *
9
- * `decision: block` makes the agent fix it in the same turn; the edit itself
10
- * already landed. Only comments absent from `tool_response.originalFile`
11
- * count, so editing a legacy file never reports its old comments.
12
- *
13
- * Fail-open: any error passes silently.
14
- */
3
+ // `decision: block` makes the agent fix it in the same turn (the edit already landed). Only comments absent from
4
+ // `tool_response.originalFile` count, so editing a legacy file never reports its old comments.
15
5
 
16
6
  import { readFileSync } from 'fs';
17
7
  import { isAbsolute, relative, resolve } from 'path';
@@ -78,10 +68,6 @@ function describe(file, findings) {
78
68
  ].join('\n');
79
69
  }
80
70
 
81
- /**
82
- * @param {object} payload - PostToolUse stdin payload
83
- * @returns {Promise<object|null>}
84
- */
85
71
  export async function run(payload) {
86
72
  try {
87
73
  if (!['Write', 'Edit'].includes(payload?.tool_name)) return null;