@polymorphism-tech/morph-spec 8.47.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,71 +1,500 @@
1
1
  # morph-spec CLI — Referência Completa
2
2
 
3
- > Referência completa (flags, semântica, exemplos) dos comandos `morph-spec` usados pela LLM em vez de editar o estado à mão. Resumo rápido em `.morph/framework/CLAUDE.md`; racional dos gates em `.morph/framework/MORPH.md` §5, dispatch de sub-agents em §6, worktree/fleet em §2.2.
3
+ > Referência completa dos comandos `morph-spec`, um `###` por comando: a assinatura, a referência (flags, exit codes, saída) e, quando há, o bloco **Por quê**. Leia a seção do comando que vai usar, não o arquivo inteiro. Papéis, fases e gates: `.morph/framework/MORPH.md`.
4
4
 
5
5
  ---
6
6
 
7
7
  ## Gates e estado
8
8
 
9
- - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando). **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.
10
- - **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta). Ao encerrar a **uiux**, a assinatura `auto` é recusada (exit 1) pelo mesmo predicado do `approve` e do `gate-check uiux`: checklist de tela incompleto no `1-design/mockups.md` — toda `## Tela:` com o `### Checklist de tela` respondido item a item (`src/lib/specs/screen-checklist.js`), com a `Superfície` sendo uma das `surfaces` da `ui-designer` no `agents.json` instalado. Destravam a decisão gravada com `pause: false` (`gate-decision`, e só para o mockup que ela viu) e a assinatura humana (`--approver <nome>`); `--force` não dispensa o checklist, como no `approve`. Quem avança sobre o checklist incompleto com `--approver` vê o que ficou faltando. Sem `mockups.md`, ou com o mockup no formato antigo (telas `## S001:`, sem `## Tela:`), não há o que cobrar.
11
- - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations` — qualquer outra chave é ignorada com aviso. No **review**, o `gate-check` lê o veredito do `evaluator-report.md` (`evaluatorReportState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.evaluatorVerdict = {status, verdict, blockers, untyped, superseded}`: `**Verdict:** SHIP` sem bloqueador aberto passa; `FIX`, bloqueador aberto, bloqueador sem tipo (`uc`/`teste`/`regressao`/`seguranca`) ou relatório sem `**Verdict:**` **pausa o Gate 3**. O relatório é append-only e vale o ÚLTIMO `**Verdict:**`; vereditos anteriores aparecem em `superseded`. `evaluatorVerdict` não é sobrescrevível por `--signals`. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. 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`/`proposal` 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). **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 `scope` ("task group … does not touch runtime") 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. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **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 task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco 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"). 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. **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 `evaluator-report.md`, e a linha que o avaliador 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`.
12
- - **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
13
- - **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
9
+ ### approve
10
+
11
+ **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`plan`/`uiux`/`review`) em `trust: auto`.
12
+
13
+ **O Gate 3 exige os relatórios de review**: `4-review/verifier.md` e `reviewer.md`, mais `security.md` quando o diff toca caminho sensível (`securityReviewFor`) — ausente, PARCIAL ou sem `**Verdict:**`, o comando recusa (exit 1) nomeando o arquivo. A assinatura humana (`--approver`) aprova mesmo com `FIX` ou cenário não provado; a `auto` recalcula a pausa do `gate-check`, cobertura dos `Cn` incluída. `**Reviewed-SHA:**` é lido de todo relatório presente e o pior vence: o código andou depois dele → recusa; sem carimbo → aviso. Se o dispatch for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate).
14
+
15
+ **O plano semeia `bin/`/`obj/`** (`plan-check` que grava o `checks.json`, `approve <f> plan`, `advance` saindo do plano e `advance --design` com o plano já aprovado — o `plan-check` cobre também o gate gravado pelo hook state-sync): num projeto .NET — `project.stack` com `dotnet`, fatia de `dotnet-senior`/`ef-modeler`/`maf-expert` no `checks.json` ou `.csproj`/`.sln`/`.slnx` na árvore (fora de `worktrees/`, `node_modules/`, `bin/`, `obj/`) — as duas linhas entram no `.gitignore` da árvore dona antes do primeiro código, e a saída as nomeia para irem no mesmo commit. O `create` aplica a mesma regra (`src/lib/stack/dotnet-build-ignores.js`); no greenfield só a fatia do plano existe para decidir.
16
+
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
+
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
+
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
+
23
+ **Registre quem aprovou (`approvedBy`):** objeto `{ approved, timestamp, approvedBy }`. `"auto"` — política `trust: auto` sem decisão humana. `"manual"` — humano aprovou — via `AskUserQuestion` ou, em headless, texto; o hook só reconhece resposta que começa com "aprovado" ou contém "pode avançar", "pode implementar" ou "pode prosseguir" (outra frase, ex. "lgtm", não grava: aprove por `morph-spec approve`); em `trust: manual` todo gate é `"manual"` — nunca `"auto"` num gate humano.
24
+
25
+ **O hook gate-guard.js** confere, sem bloquear, se a ação num gate (pausar vs auto-aprovar) bateu com a decisão persistida. **Default:** `gate-check <feature> <gate> [--signals <json>] [--json]` decide via `shouldPauseGate`; `--signals` sobrescreve só o que o disco não expõe (`designSystemDivergence`, `standardsViolations`). **Escape manual** (sinal que `shouldPauseGate` não representa): `gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`. Ambos gravam `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt, source }` server-side (`.morph/framework/schemas/feature.schema.json`). **Pausa manual é pegajosa:** com `source: manual` e `pause: true` vigentes, o `gate-check` mantém a pausa e o motivo humano (grava só os sinais recalculados); quem a libera é um novo `gate-decision --pause false`. Divergência → o guard avisa no contexto.
26
+
27
+ **Por quê.** **Nunca edite `gateDecisions` à mão** — colide com state-sync.
28
+
29
+ ### advance
30
+
31
+ **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux depois do plano e escreve `1-design/mockups.md` a partir do template do pacote e, se o projeto não tiver, `.morph/context/design-system.md`; nunca sobrescreve). Uma decisão gravada com `pause: true` só se fecha por assinatura humana (`--approver`) ou por um novo `gate-decision --pause false`, pela mesma regra do `approve` (`src/commands/state/auto-approval.js`). Sair de **implement** exige toda fatia do `checks.json` com relatório final em `3-implement/reports/F{n}.md` (ausente ou `PARCIAL` recusa, nomeando o arquivo; sem `checks.json`, recusa nomeando-o); `--force` avança e diz o que pulou. Ao encerrar a **uiux**, a assinatura `auto` é recusada (exit 1) pelo mesmo predicado do `approve` e do `gate-check uiux`: checklist de tela incompleto no `1-design/mockups.md` — toda `## Tela:` com o `### Checklist de tela` respondido item a item (`src/lib/specs/screen-checklist.js`), com a `Superfície` sendo uma das `surfaces` da `ui-designer` no `agents.json` instalado. Destravam a decisão gravada com `pause: false` (`gate-decision`, e só para o mockup que ela viu) e a assinatura humana (`--approver <nome>`); `--force` não dispensa o checklist, como no `approve`. Quem avança sobre o checklist incompleto com `--approver` vê o que ficou faltando. Sem `mockups.md`, ou com o mockup no formato antigo (telas `## S001:`, sem `## Tela:`), não há o que cobrar.
32
+
33
+ ### gate-check
34
+
35
+ **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `sliceCount` — mais de 12 fatias pausa —, `planChecks` — só no `plan`: `checks.json` ausente, ilegível ou extraído de outro `plan.md` (`plan-check` não rodou até zero erro) pausa, e `approve`/`advance` automáticos recusam pela mesma regra (`planChecksPause`) —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations` — qualquer outra chave é ignorada com aviso.
36
+
37
+ No **review**, o `gate-check` lê os relatórios de `4-review/` (`src/lib/review-reports.js` — leitor único) e grava `signals.reviewReports = {required, reports: {verifier, reviewer, security}, checks}`: exigidos são `verifier` e `reviewer`, mais `security` quando o diff toca caminho sensível; no `hotfix`, só `verifier`. Pausa o Gate 3: relatório exigido ausente, PARCIAL ou sem `**Verdict:**`; `FIX`; bloqueador aberto; bloqueador sem tipo (`check`/`teste`/`regressao`/`seguranca`/`escopo`/`arquitetura`/`performance`); e a cobertura — todo cenário do `checks.json` precisa de uma linha `PROVADO` na `## Checks` do `verifier.md` — somadas todas as seções, vale a última linha de cada Cn (`checks.missing` = sem linha, `checks.notProven` = `NÃO PROVADO`/`FALHA`), e `checks.json` ilegível também pausa. Cada relatório é append-only e vale o ÚLTIMO `**Verdict:**`. `reviewReports` não é sobrescrevível por `--signals`. Fora do review a regra não vale.
38
+
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
+
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`.
42
+
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
+
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
+
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
+
49
+ #### Sinais
50
+
51
+ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imprime quando pausa. Só `designSystemDivergence` e `standardsViolations` entram por `--signals`; risco que nenhum sinal representa vai por `gate-decision`.
52
+
53
+ | Sinal | Threshold | Quando aplica |
54
+ |---|---|---|
55
+ | `designSystemDivergence` | true | Fontes de design conflitantes |
56
+ | `standardsViolations` | array.length > 0 | Plano ou código viola padrões proibidos |
57
+ | `reviewReports` | ≠ SHIP, bloqueador, `Cn` sem PROVADO, ou ausente | Relatórios exigidos: `FIX`, bloqueador aberto ou sem tipo, PARCIAL, sem `**Verdict:**`, ou cenário sem linha `PROVADO` em `## Checks`. Só no **ship** |
58
+ | `verificationStatus` | === 'fail' | `verify` vermelho — build, testes, validadores **ou e2e**. Vale em **qualquer** gate |
59
+ | `verificationStatus` | ausente, no `review` | nenhum `verify` carimbado: ship sem prova; `approve` auto aplica a mesma régua |
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** |
62
+ | `promptModel` | prompt/modelo tocado | Sem nó `evals` declarado, ou eval pulado neste verify. Só no **ship** |
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, nenhuma `## Tela:`, ou tela sem protótipo e prints 1280/390. **ship**: item decidido sem linha em `## Checklist de tela` |
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
+ | `sliceCount` | > 12 | Plano grande demais para uma feature |
67
+ | `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
68
+
69
+ | Gate | Sinais | Skill responsável |
70
+ |---|---|---|
71
+ | Plano | `designSystemDivergence`, `standardsViolations`, `planChecks`, `sliceCount` | `morph-plan` |
72
+ | uiux — Design | `screenChecklist` | `morph-uiux` |
73
+ | 3 — Review | `reviewReports`, `standardsViolations` em implementation, `verificationStale`, `e2eSkipClass`, `promptModel`, `mediaClaims`, `screenChecklist` | `morph-review` |
74
+
75
+ ### gate-decision
76
+
77
+ **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
78
+
79
+ ### Guarda de árvore dona
80
+
81
+ **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
14
82
 
15
83
  ## Features
16
84
 
17
- - **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree] [--no-checkout]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos. **E sem `--worktree` o checkout PRINCIPAL se move para essa branch**: o comando avisa em amarelo, nomeando a branch anterior e a nova (sob `--json` o aviso vai para **stderr**, para sobreviver ao redirecionamento do stdout), porque trabalho não commitado vai junto e todo commit seguinte naquela árvore cai na branch da feature. `--no-checkout` cria a branch **sem** mover a árvore (a branch existe de todo jeito, para o `finish --pr|--merge` continuar simétrico); a saída estruturada distingue os dois casos em `branch.checkedOut` (+ `branch.movedFrom` / `branch.stayedOn`). Para trabalho em paralelo o caminho recomendado continua sendo `--worktree`, que não toca no checkout da raiz.
18
- - **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
19
- - **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
20
- - **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
85
+ ### create
86
+
87
+ **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree] [--no-checkout]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início do plano, `morph-plan` §1; o hook state-sync continua cobrindo o caminho implícito como fallback).
88
+
89
+ **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio.
90
+
91
+ **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
92
+
93
+ **E sem `--worktree` o checkout PRINCIPAL se move para essa branch**: o comando avisa em amarelo, nomeando a branch anterior e a nova (sob `--json` o aviso vai para **stderr**, para sobreviver ao redirecionamento do stdout), porque trabalho não commitado vai junto e todo commit seguinte naquela árvore cai na branch da feature. `--no-checkout` cria a branch **sem** mover a árvore (a branch existe de todo jeito, para o `finish --pr|--merge` continuar simétrico); a saída estruturada distingue os dois casos em `branch.checkedOut` (+ `branch.movedFrom` / `branch.stayedOn`).
94
+
95
+ **Roteador (`--request`).** sinais determinísticos primeiro — `morph-spec create --request "<texto>"` roda o detector por keywords (hotfix: "produção caiu/urgente/incident/p0"; bug: "bug/corrigir/exception/regressão"; chore: "typo/rename/bump/config/docs/cleanup"). Confiança `high` → usa o tipo. Empate (`tie`) ou nenhum sinal (`none`) → o comando sai com código 2 e os candidatos; **você decide como roteador** e re-roda com `--type`. Só vale uma `AskUserQuestion` quando o pedido é genuinamente indeterminado.
96
+
97
+ **Por quê.** Para trabalho em paralelo o caminho recomendado continua sendo `--worktree`, que não toca no checkout da raiz. As keywords casam como **palavra inteira** ("group0" não é `p0`, "prefix" não é `fix`, "debug" não é `bug`) — casar demais pularia gates num pedido inócuo; por isso plural e flexão são listados explicitamente ("bugs", "incidentes", "configs").
98
+
99
+ ### list
100
+
101
+ **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate).
102
+
103
+ Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda).
104
+
105
+ Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting.
106
+
107
+ **Por quê.** É o complemento do `delete` (enxergar o que está morto antes de remover).
108
+
109
+ ### delete
110
+
111
+ **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
112
+
113
+ ### retype
114
+
115
+ **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
116
+
117
+ Feature sem `workType` (pré-v8) é tratada como `feature`.
21
118
 
22
119
  ## Worktree / Fleet
23
120
 
24
- - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação. **Toda saída de sucesso carimba `memoryNote`**: a frase que nomeia o que o fechamento NÃO deixa registrado no repositório — decisão do usuário, pendência a observar em produção, custo medido. O commit de archive, o `decisions.md`, os use cases promovidos e o `git log` já guardam o resto, e memória de sessão escrita por cima desses fatos nasce redundante e envelhece sem revisão. Recusa não a carimba: não há fechamento sobre o qual gravar nada.
25
- - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
26
- - **`morph-spec usecase check <feature> [--json]`** — o ciclo de vida do acervo contra a feature, rodável antes de existir `tasks.json` (o `spec.md` congela no Gate 2, então é aqui que uma linha de revogação errada ainda tem conserto): task cujo `usecase` está `superseded` (no acervo, na cópia da feature ou revogado pelo próprio `spec.md`), frontmatter de supersessão malformado (`superseded` sem `superseded_by` UC-{nn}, `superseded_by`/`scope` sem `status: superseded`) e linha de revogação que o `finish` não aplicaria (id malformado, UC fora do acervo, substituto inexistente, ADR fora do índice). Avisos, exit 0. Com `tasks.json`, roda também o cruzamento task × use case do `dag` — e sai 1 onde o `dag` sairia (UC inexistente, schema inválido). `--json`: `{feature, useCases, lifecycle: {warnings}}`.
27
- - **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
28
- - **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). `remove` recusa, antes de tocar em qualquer junction, quando o cwd do processo está no alvo (`cwd-inside-worktree`) **ou quando a sessão do Claude Code que o rodou está ancorada nele** (`session-anchored-in-worktree` — mesmo sinal do `finish`, acima); a recusa é o adiamento: feche a sessão e rode de uma aberta na raiz. Falha de remoção reporta a CAUSA real: um diretório travado vence o eco `is not a working tree` das tentativas seguintes, e `alreadyDeregistered` avisa quando o git já desregistrou a árvore e só o diretório vazio sobrou. **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
- **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
30
- **Base da árvore de task.** Com `worktree.baseRef` ausente nas settings e o cwd de quem despacha numa branch `morph/{f}`, o hook passa `--base morph/{f}`: o sub-agent do apply nasce com o trabalho da feature (nada de `git merge --ff-only morph/{f}` como passo 0). `baseRef: "head"` usa o que o cwd tiver checado; `baseRef: "fresh"` explícito força a branch default. Com base `morph/{f}`, o `worktree.env` grava `MORPH_FEATURE={f}` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
31
- **Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. **Um projeto cujo compose lê a porta por outro nome** (`${PG_PORT}` em vez de `POSTGRES_PORT`) declara `e2e.portEnvNames` no `config.json` (`{ "postgres": "PG_PORT" }`; chaves `web`, `api`, `postgres`, `redis`): o nome declarado sai com o **mesmo valor** em todo lugar do layout e, sendo layout, o `envFile` de credenciais não o sobrescreve (vale o mesmo aviso em stderr). Entrada que não dá para honrar (porta desconhecida, nome inválido ou reservado, nome repetido) é descartada e nomeada em stderr. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
32
- - **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
33
- - **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido. **Nada é commitado na branch principal por causa do esqueleto da feature na raiz:** o registro `worktree` é apagado do `feature.json`, e só um registro que já estava **rastreado** vira commit. O esqueleto não rastreado de uma feature já arquivada na raiz é apagado com backup (`shedBackup`); o de uma feature sem archive fica como está.
34
- - **`morph-spec worktree reap [--dry-run] [--grace <min>] [--json]`** — remove as **sobras de sub-agent**: o worktree `agent-<hex>` (branch `morph-task/agent-<hex>`, o que o `Agent` com `isolation: "worktree"` deixa para trás) e a branch órfã `morph-task/agent-<hex>`. **Só remove se TODAS as provas fecham; na dúvida preserva e lista com a razão:** (1) é de agent — diretório `agent-<hex>` sob `worktrees/` e branch `morph-task/agent-<hex>` (ou HEAD destacado); `morph-task/<nome>`, features e `claude -w` nunca entram; (2) o agente **parou** — o último evento do `.morph/logs/events.jsonl` da árvore é o `AgentUsage` dele (`agentId` = o `<hex>`); sem esse evento só lista (`no-usage-event`); (3) **graça** — o log parou de crescer há ≥ `worktree.reap.graceMinutes` (30; `--grace`), porque um agente parado ainda pode ser retomado por `SendMessage`; (4) **árvore limpa** — `git status --porcelain --untracked-files=all`; arquivo não rastreado também bloqueia (`dirty`); (5) **sem commit exclusivo** — `git rev-list <tip> --not <refs>`, com `refs` = branches locais, remotas e tags **menos todas as `morph-task/agent-*`**. Nunca `--all`: ele conta o HEAD de cada worktree e o da própria árvore protegeria o commit; e dois agentes no mesmo commit exclusivo se protegeriam um ao outro — assim preservam os dois; (6) não é o cwd nem a âncora da sessão. Medir não deu (`null`) sempre preserva. **Como remove:** pelo núcleo do `worktree remove --target --if-clean` (desmonta as junctions antes, backup do que só existe ali em `finish-backup/task/`, retenção por namespace), nunca `rm -rf`; **falha de remoção devolve as junctions** (o `worktree remove` manual as deixa desmontadas de propósito): o agente pode estar vivo, e sem `.claude` os hooks dele morrem. Depois apaga a branch (`git branch -D`, com a contagem de commits exclusivos refeita antes). **Um lock por repositório** (`<git-common-dir>/morph-spec/agent-reap.lock`) serializa dois reapers: sem ele, o relink de um recriaria junctions no meio da remoção do outro, e o `--force` as atravessaria; lock de pid vivo → `busy: true` e nada é feito. Branch órfã sai pela mesma regra 5, com a graça medida pelo reflog da branch (o `provision` cria a branch antes do worktree). Idempotente: a 2ª rodada não acha nada. **Quem dispara:** as skills (`morph-apply` §3b-w passo 4, `morph-implement` pós-dispatch, `morph-review` depois de ler o `evaluator-report`), o SessionStart (`worktree-context` solta um worker destacado, só na raiz, no máximo 1 a cada 10 min e só se houver `agent-<hex>` no disco; `MORPH_WORKTREE_REAP=0` desliga) e o `doctor --worktrees --prune`. `--json`: `{ ok, dryRun, graceMinutes, reaped[], branchesDeleted[], wouldReap[] (só no dry-run), preserved[{path,branch,hex,reason,detail}], active, busy? }`. **Telemetria:** remover a árvore leva o `events.jsonl` dela (o custo do agent em `cost --all-trees`); o conteúdo de `.morph/` fica no backup `finish-backup/task/`, como já ficava no `worktree remove --target` do `morph-apply`.
35
- - **`.vscode/settings.json` (editor)** — `init`, `update`, `worktree setup|provision` e `doctor --worktrees --prune` adicionam a `files.watcherExclude`, `search.exclude` e `files.exclude` os globs `**/worktrees/**` (e o legado `**/.worktrees/**`) e `**/node_modules/**`; `**/bin/**` e `**/obj/**` entram em `watcherExclude` e `search.exclude` sempre e em `files.exclude` só em projeto .NET (stack `dotnet` na config, ou `.sln`/`.slnx`/`.csproj` na raiz), porque num projeto JS o `bin/` guarda código-fonte. **Só adiciona o que falta:** chave do usuário, ordem e valor existente ficam (`"**/bin/**": false` continua `false`), e a 2ª rodada não muda um byte. **JSONC:** não há escritor que preserve comentário, então arquivo **com comentário, com vírgula sobrando, ilegível, com raiz que não é objeto, ou rastreado pelo git** não é escrito — o relatório (`editorSettings` no `--json` de `setup`/`provision`, `prune.editorSettings` no do `doctor`) traz `status: "suggest"`, a `reason` e o `snippet` só com o que falta, para colar. Legível e sem comentário, reescreve preservando BOM, EOL e indentação. **O arquivo criado** entra no `info/exclude` do common dir (`git-exclude.js`): não rastreado, contaria como sujeira da raiz, e o `finish --merge` recusa raiz suja. Desliga: `worktree.editorSettings: false` na config, ou `MORPH_EDITOR_SETTINGS=0`. O `update --dry-run` prevê a escrita (`writeFile`). **O que NÃO cobre:** as chaves cortam watcher, busca e explorer — não governam a descoberta do Git nem o language server. `git.repositoryScanMaxDepth` (padrão 1) não alcança `worktrees/<n>` (profundidade 2), e `git.detectWorktrees` vem **desligado** por padrão (`false`, `scope: resource`; lido no `package.json` da extensão Git do VS Code 1.124.2) e é um recurso de LISTAGEM: mostra as worktrees em *Source Control Repositories*, até `git.detectWorktreesLimit` (50). Ligá-lo faz o VS Code enxergar mais repositórios, não menos, e a escrita no workspace sobrepõe uma preferência global do usuário — por isso o morph-spec não o grava; quem o quer liga na própria config. Não medido: se o C# Dev Kit respeita `files.exclude`, e o antes/depois de RAM e de processos do VS Code com as exclusões.
36
- - **`worktree.root` (worktrees fora da árvore aberta)** — chave de `.morph/config/config.json` (ou `MORPH_WORKTREE_ROOT`, por rodada): absoluta, `~/…` ou relativa à raiz primária. As worktrees **novas** (`setup`, `provision`, o hook `WorktreeCreate` e o fallback dele em git puro) nascem em `<root>/<repo>-<hash>/<nome>`; o hash é da raiz do repositório, para dois repositórios de mesmo nome dividirem o mesmo root sem colidir. **Sem a chave, nada muda.** **Só aceita root FORA da árvore:** um valor que resolve para dentro da raiz do projeto é **ignorado com aviso** (`worktreeRootWarning` no `--json` de `setup`/`provision`) e valem `worktrees/`, porque toda lista orientada por esse nome (`.gitignore`, `graphifyignore`, descoberta de testes, o `tar` do `--linux`, os globs do editor) deixaria de enxergá-lo em silêncio. Um root **acima** da raiz (`..`) é aceito: o contêiner vira um irmão do repositório. **"Está no contêiner" é a UNIÃO** do layout configurado com `worktrees/` e `.worktrees/`: a árvore que já existe continua reconhecida depois que a chave é ligada ou trocada, e uma criada em qualquer outro lugar segue acusada como `out-of-container`. O resolvedor é um só (`framework/hooks/shared/worktree-root.js`; hook não importa `src/`) e é lido por `buildWorktreePath`, pelo mkdir do pai e por `findWorktreeForFeature`; pelo `doctor --worktrees` (`inContainer` e a varredura de resíduo, que olha só `<root>/<repo>-<hash>/` — nunca a pasta de outro repositório, senão o `--prune` apagaria pastas alheias); pelo reaper; pelo SessionStart (`isOutsideContainer`); pelo registro de projetos (a worktree é detectada pelo `.git` ser um arquivo que aponta para `.git/worktrees/`, não pelo nome do diretório pai); e pelos scanners do dashboard. `finish`, `remove` e `list` andam pelo `git worktree list` e não dependem do lugar. **Não faz:** trocar a chave não move as worktrees que já existem.
37
- - **Modelo de trabalho: o Claude Code é a porta de entrada, o morph é o dono da worktree** — `claude -w <nome>`, o `EnterWorktree` (por nome) e o `isolation: "worktree"` de sub-agent passam pelo hook `WorktreeCreate` do morph. Segundo a doc do Claude Code, o hook **substitui por inteiro** a criação nativa (o `git worktree add`, o caminho e a branch) e pode devolver **qualquer** caminho na 1ª linha do stdout (o payload traz `name`, `worktree_path` sugerido, `branch_name` e `source_repo_path`; o morph lê `name`). Por isso entram como `worktree setup <f>` (nome de feature conhecido) ou `worktree provision <n>` (o resto), já com a infra, o bloco de portas e o `morph/…`/`morph-task/…`. **O que a criação nativa daria e o morph faz por conta própria, porque o hook a substitui:** `worktree.baseRef` (o hook lê as três camadas de settings), `.worktreeinclude` (a doc: com hook ele "não é processado"; `copyWorktreeIncludes` o honra — o que casa o padrão **e** é ignorado na árvore, nunca `node_modules`/artefato de build/`.git` aninhado, nunca sobrescreve, idempotente; o `provision` continua copiando `.env*` e o resultado sai em `worktreeInclude` no `--json`; **custo medido:** 0,15 s numa raiz com 6 mil arquivos em `node_modules` e 0,5 s (1,4 s a frio) no MORPH_GHLBrain, contra o timeout de 120 s do `WorktreeCreate`, e um pathspec de exclusão não poda a caminhada; **nunca `--exclude-standard`**, que com `--ignored` devolve todo o ignorado — 57 560 caminhos no GHLBrain em vez dos 4 que casam) e a cópia de `.env*`. **Abrir uma worktree que já existe:** abra o `claude` DENTRO dela — uma feature = um worktree = uma instância. `EnterWorktree(path)` para um caminho fora de `.claude/worktrees/` pede aprovação **a cada vez** (regra de permissão e "não perguntar de novo" não a suprimem; só `bypassPermissions`), e uma sessão que entra no meio mantém `CLAUDE_PROJECT_DIR` na raiz e move só o `cwd` do payload: por isso o SessionStart do morph (`worktree-context.js`, `sessionDir`) segue o `cwd` quando ele é um worktree vinculado do MESMO repositório, e a identidade, o bloco de portas e o handoff chegam. **Limpeza:** o sweep periódico nativo mantém qualquer worktree sem o marcador que o Claude grava ("including a worktree a WorktreeCreate hook created"), então **nunca** limpa as do morph; quem limpa é o `worktree reap`, que respeita `git worktree lock` (a marca que o Claude usa em agente rodando) e devolve `locked` como razão. O `WorktreeRemove` (fim de sub-agent e saída de sessão) chama `worktree remove --if-clean`; ele não dispara em `-p` (medido), e é esse o resíduo que o reaper pega. **Isolamento nativo:** numa sessão isolada por `--worktree`/`EnterWorktree` o Claude bloqueia edição, `cwd` de comando e redirect de git (`git -C`, `GIT_DIR`, `cd` para a raiz) para o checkout principal — `finish`, `worktree remove --target` e `worktree reap` rodam da raiz, não de dentro. A remoção nativa não atravessa mais junction no Windows (v2.1.205), mas o `git worktree remove` cru continua atravessando: a regra "só o morph remove" segue valendo. **Não medido:** se o SessionStart de `claude -w` já chega com o `cwd` no worktree, e o `claude -w "#1234"` (PR): o hook recebe um `name` e cai no `--base` do morph, não no head do PR. Fontes: `code.claude.com/docs/en/worktrees` e `/hooks`.
38
- - **Backups do finish (`.git/morph-spec/finish-backup/`) e a retenção** — o que o `finish` (o shed da raiz, `{f}-{carimbo}/`) e o desmonte do worktree (`{f}-{carimbo}-worktree-ignored/`) copiam antes de apagar um arquivo que não existe em outro lugar; `worktree remove` de árvore de task grava em `task/{n}-{carimbo}-worktree-ignored/`. Cada pasta ganha um manifesto ao lado (`{pasta}.manifest.json`): o que foi copiado, os caches deixados de fora (`excludedCaches`) e os **nomes** dos `.env*` que entraram (`secrets`). **Um finish é UM backup**: as duas pastas dele levam o mesmo carimbo e contam juntas; pastas gravadas antes disso têm carimbos diferentes e contam uma a uma. **Retenção ligada por padrão**, também em projeto cujo `config.json` não tem a chave: os **20 backups mais novos de cada namespace** (a raiz de `finish-backup/` e `task/` têm orçamentos separados — um fan-out de sub-agents não despeja o backup da feature fechada um minuto antes) **sempre ficam, de qualquer idade** — não há corte por idade no padrão, então um período parado nunca zera os backups das últimas features fechadas —; dentro de um backup que fica, os arquivos `.env*` são apagados depois de **7 dias** (o resto fica, e o manifesto guarda os nomes e `secretsStrippedAt`). Configurável em `config.json → finishBackup.retention: { maxCount, maxAgeDays, secretMaxAgeDays }` (inteiros ≥ 1; `maxAgeDays` é um corte por idade OPCIONAL — ausente ou `null`, o padrão, não corta) ou desligada com `finishBackup.retention: false` (schema em `framework/schemas/config.schema.json`, autoridade em `resolveBackupRetention()`). **O que não dá para ler PULA a poda naquela execução, nunca cai no padrão** — é uma operação que apaga, e um `{ "enabled": false }`, um `"false"`, um `maxCount: 0`, uma chave desconhecida, um `finishBackup` que não é objeto ou um `config.json` que não parseia podem muito bem querer dizer "guarde tudo": a saída diz `backupRetention: {skipped: "<razão>"}`, o `doctor --worktrees --prune` avisa com a razão, e o `doctor`/`doctor --worktrees` acusam até alguém corrigir. Só a ausência da chave liga o padrão. **Backup sem manifesto é legado** (gravado antes desta retenção) — esse é o critério, e nenhum outro. A poda automática nunca o apaga, nunca tira os `.env*` dele e não lhe dá vaga no orçamento; ela só conta quantos esperam (`backupRetention.legacyPending: N` + `legacyHint`). **A primeira poda deles é só com `doctor --worktrees --prune` explícito**, que os ranqueia junto com os demais (contando no teto), apaga os que ficam fora e grava um manifesto (`legacy: true`, `adoptedAt`) em cada um que mantém — dali em diante são backups comuns. A poda automática roda **depois** do backup novo — no fim de todo `finish` com `finished: true` e de todo `worktree remove` que gravou backup (a saída traz `backupRetention: {removed, removedSample, secretsStripped, declined, legacyPending, legacyHint}` só com o que se aplica). **O backup que a execução acabou de gravar nunca é podado por ela** — o carimbo dela é protegido —, e backup com carimbo no FUTURO (relógio adiantado e depois corrigido) fica e não ocupa vaga na contagem. O `finish --dry-run` anuncia só o que a poda AUTOMÁTICA faria (`backupRetention: {outsideRetention, overdueSecrets, legacyPending}` + uma linha em `wouldDo`), sem apagar nada. Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o resto do desmonte: backup com reparse point dentro é mantido (`declined`) — mas perde os `.env*`, porque a política o queria fora —, link nunca é atravessado (nem um `task/` que seja junction), e **só `task/` é namespace**: qualquer outra pasta de primeiro nível que não tenha nome de backup nunca é visitada nem removida, nem vazia. O `.env*` idêntico ao da raiz nunca chega a ser copiado (é replicável, ver acima); o que chega é o que difere — o `.env.e2e` de cada worktree carrega o bloco de portas dela, então nenhum é igual ao da raiz, e é por isso que o prazo curto existe.
39
- - **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
40
- - **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
41
- - **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
42
- - **`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.
121
+ ### finish
122
+
123
+ **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
124
+
125
+ **Índice de ADRs:** no mesmo commit do archive, o `finish` lê o `2-plan/plan.md` da feature: os ADRs de `## Decisões` com `**Alcance:** projeto` entram em `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo), e a tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` retira (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os ADRs revogados — forma em `morph-spec plan-check --help`; o `plan-check` avisa, enquanto o plano ainda muda, a linha que o `finish` não aplicaria. O payload traz `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`.
126
+
127
+ `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`.
128
+
129
+ **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. O `--dry-run` já avisa (`teardownDeferred` no pré-voo).
130
+
131
+ **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada.
132
+
133
+ Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
134
+
135
+ **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
136
+
137
+ **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
138
+
139
+ **Toda saída de sucesso carimba `memoryNote`**: a frase que nomeia o que o fechamento NÃO deixa registrado no repositório — decisão do usuário, pendência a observar em produção, custo medido. Recusa não a carimba: não há fechamento sobre o qual gravar nada.
140
+
141
+ **Por quê.** Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O commit de archive, o `plan.md` com as decisões, o índice de ADRs e o `git log` já guardam o resto, e memória de sessão escrita por cima desses fatos nasce redundante e envelhece sem revisão.
142
+
143
+ ### archive
144
+
145
+ **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
146
+
147
+ ### worktree setup, provision, link, list
148
+
149
+ **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). `remove` recusa, antes de tocar em qualquer junction, quando o cwd do processo está no alvo (`cwd-inside-worktree`) **ou quando a sessão do Claude Code que o rodou está ancorada nele** (`session-anchored-in-worktree` — mesmo sinal do `finish`, acima); a recusa é o adiamento: feche a sessão e rode de uma aberta na raiz. Falha de remoção reporta a CAUSA real: um diretório travado vence o eco `is not a working tree` das tentativas seguintes, e `alreadyDeregistered` avisa quando o git já desregistrou a árvore e só o diretório vazio sobrou. **Fechar a feature** continua sendo `morph-spec finish` (acima).
150
+
151
+ O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a cada load (self-heal) e nunca é sincronizado à mão.
152
+
153
+ **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
154
+
155
+ **Base da árvore de task.** Com `worktree.baseRef` ausente nas settings e o cwd de quem despacha numa branch `morph/{f}`, o hook passa `--base morph/{f}`: o sub-agent do apply nasce com o trabalho da feature (nada de `git merge --ff-only morph/{f}` como passo 0). `baseRef: "head"` usa o que o cwd tiver checado; `baseRef: "fresh"` explícito força a branch default. Com base `morph/{f}`, o `worktree.env` grava `MORPH_FEATURE={f}` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
156
+
157
+ **Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. **Um projeto cujo compose lê a porta por outro nome** (`${PG_PORT}` em vez de `POSTGRES_PORT`) declara `e2e.portEnvNames` no `config.json` (`{ "postgres": "PG_PORT" }`; chaves `web`, `api`, `postgres`, `redis`): o nome declarado sai com o **mesmo valor** em todo lugar do layout e, sendo layout, o `envFile` de credenciais não o sobrescreve (vale o mesmo aviso em stderr). Entrada que não dá para honrar (porta desconhecida, nome inválido ou reservado, nome repetido) é descartada e nomeada em stderr. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
158
+
159
+ ### env
160
+
161
+ **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
162
+
163
+ ### worktree remove
164
+
165
+ **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho.
166
+
167
+ Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria).
168
+
169
+ **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
170
+
171
+ **Nada é commitado na branch principal por causa do esqueleto da feature na raiz:** o registro `worktree` é apagado do `feature.json`, e só um registro que já estava **rastreado** vira commit. O esqueleto não rastreado de uma feature já arquivada na raiz é apagado com backup (`shedBackup`); o de uma feature sem archive fica como está.
172
+
173
+ **Por quê.** A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho.
174
+
175
+ ### worktree reap
176
+
177
+ **`morph-spec worktree reap [--dry-run] [--grace <min>] [--json]`** — remove as **sobras de sub-agent**: o worktree `agent-<hex>` (branch `morph-task/agent-<hex>`, o que o `Agent` com `isolation: "worktree"` deixa para trás) e a branch órfã `morph-task/agent-<hex>`.
178
+
179
+ **Só remove se TODAS as provas fecham; na dúvida preserva e lista com a razão:** (1) é de agent — diretório `agent-<hex>` sob `worktrees/` e branch `morph-task/agent-<hex>` (ou HEAD destacado); `morph-task/<nome>`, features e `claude -w` nunca entram; (2) o agente **parou** — o último evento do `.morph/logs/events.jsonl` da árvore é o `AgentUsage` dele (`agentId` = o `<hex>`); sem esse evento só lista (`no-usage-event`); (3) **graça** — o log parou de crescer há ≥ `worktree.reap.graceMinutes` (30; `--grace`), porque um agente parado ainda pode ser retomado por `SendMessage`; (4) **árvore limpa** — `git status --porcelain --untracked-files=all`; arquivo não rastreado também bloqueia (`dirty`); (5) **sem commit exclusivo** — `git rev-list <tip> --not <refs>`, com `refs` = branches locais, remotas e tags **menos todas as `morph-task/agent-*`**. Nunca `--all`: ele conta o HEAD de cada worktree e o da própria árvore protegeria o commit; e dois agentes no mesmo commit exclusivo se protegeriam um ao outro — assim preservam os dois; (6) não é o cwd nem a âncora da sessão. Medir não deu (`null`) sempre preserva.
180
+
181
+ **Como remove:** pelo núcleo do `worktree remove --target --if-clean` (desmonta as junctions antes, backup do que só existe ali em `finish-backup/task/`, retenção por namespace), nunca `rm -rf`; **falha de remoção devolve as junctions** (o `worktree remove` manual as deixa desmontadas de propósito): o agente pode estar vivo, e sem `.claude` os hooks dele morrem. Depois apaga a branch (`git branch -D`, com a contagem de commits exclusivos refeita antes).
182
+
183
+ **Um lock por repositório** (`<git-common-dir>/morph-spec/agent-reap.lock`) serializa dois reapers: sem ele, o relink de um recriaria junctions no meio da remoção do outro, e o `--force` as atravessaria; lock de pid vivo → `busy: true` e nada é feito. Branch órfã sai pela mesma regra 5, com a graça medida pelo reflog da branch (o `provision` cria a branch antes do worktree). Idempotente: a 2ª rodada não acha nada.
184
+
185
+ **Quem dispara:** as skills (`morph-apply` §4, depois de cada retorno do worktree rolling, e `morph-review` depois de ler os relatórios), o SessionStart (`worktree-context` solta um worker destacado, só na raiz, no máximo 1 a cada 10 min e só se houver `agent-<hex>` no disco; `MORPH_WORKTREE_REAP=0` desliga) e o `doctor --worktrees --prune`.
186
+
187
+ `--json`: `{ ok, dryRun, graceMinutes, reaped[], branchesDeleted[], wouldReap[] (só no dry-run), preserved[{path,branch,hex,reason,detail}], active, busy? }`.
188
+
189
+ **Telemetria:** remover a árvore leva o `events.jsonl` dela (o custo do agent em `cost --all-trees`); o conteúdo de `.morph/` fica no backup `finish-backup/task/`, como já ficava no `worktree remove --target` do `morph-apply`.
190
+
191
+ ### .vscode/settings.json
192
+
193
+ **`.vscode/settings.json` (editor)** — `init`, `update`, `worktree setup|provision` e `doctor --worktrees --prune` adicionam a `files.watcherExclude`, `search.exclude` e `files.exclude` os globs `**/worktrees/**` (e o legado `**/.worktrees/**`) e `**/node_modules/**`; `**/bin/**` e `**/obj/**` entram em `watcherExclude` e `search.exclude` sempre e em `files.exclude` só em projeto .NET (stack `dotnet` na config, ou `.sln`/`.slnx`/`.csproj` na raiz), porque num projeto JS o `bin/` guarda código-fonte.
194
+
195
+ **Só adiciona o que falta:** chave do usuário, ordem e valor existente ficam (`"**/bin/**": false` continua `false`), e a 2ª rodada não muda um byte.
196
+
197
+ **JSONC:** não há escritor que preserve comentário, então arquivo **com comentário, com vírgula sobrando, ilegível, com raiz que não é objeto, ou rastreado pelo git** não é escrito — o relatório (`editorSettings` no `--json` de `setup`/`provision`, `prune.editorSettings` no do `doctor`) traz `status: "suggest"`, a `reason` e o `snippet` só com o que falta, para colar. Legível e sem comentário, reescreve preservando BOM, EOL e indentação.
198
+
199
+ **O arquivo criado** entra no `info/exclude` do common dir (`git-exclude.js`): não rastreado, contaria como sujeira da raiz, e o `finish --merge` recusa raiz suja.
200
+
201
+ Desliga: `worktree.editorSettings: false` na config, ou `MORPH_EDITOR_SETTINGS=0`. O `update --dry-run` prevê a escrita (`writeFile`).
202
+
203
+ **O que NÃO cobre:** as chaves cortam watcher, busca e explorer — não governam a descoberta do Git nem o language server. `git.repositoryScanMaxDepth` (padrão 1) não alcança `worktrees/<n>` (profundidade 2), e `git.detectWorktrees` vem **desligado** por padrão (`false`, `scope: resource`; lido no `package.json` da extensão Git do VS Code 1.124.2) e é um recurso de LISTAGEM: mostra as worktrees em *Source Control Repositories*, até `git.detectWorktreesLimit` (50). Não medido: se o C# Dev Kit respeita `files.exclude`, e o antes/depois de RAM e de processos do VS Code com as exclusões.
204
+
205
+ **Por quê.** Ligá-lo faz o VS Code enxergar mais repositórios, não menos, e a escrita no workspace sobrepõe uma preferência global do usuário — por isso o morph-spec não o grava; quem o quer liga na própria config.
206
+
207
+ ### worktree.root
208
+
209
+ **`worktree.root` (worktrees fora da árvore aberta)** — chave de `.morph/config/config.json` (ou `MORPH_WORKTREE_ROOT`, por rodada): absoluta, `~/…` ou relativa à raiz primária. As worktrees **novas** (`setup`, `provision`, o hook `WorktreeCreate` e o fallback dele em git puro) nascem em `<root>/<repo>-<hash>/<nome>`; o hash é da raiz do repositório, para dois repositórios de mesmo nome dividirem o mesmo root sem colidir.
210
+
211
+ **Sem a chave, nada muda.** **Só aceita root FORA da árvore:** um valor que resolve para dentro da raiz do projeto é **ignorado com aviso** (`worktreeRootWarning` no `--json` de `setup`/`provision`) e valem `worktrees/`, porque toda lista orientada por esse nome (`.gitignore`, `graphifyignore`, descoberta de testes, o `tar` do `--linux`, os globs do editor) deixaria de enxergá-lo em silêncio.
212
+
213
+ **"Está no contêiner" é a UNIÃO** do layout configurado com `worktrees/` e `.worktrees/`: a árvore que já existe continua reconhecida depois que a chave é ligada ou trocada, e uma criada em qualquer outro lugar segue acusada como `out-of-container`.
214
+
215
+ O resolvedor é um só (`framework/hooks/shared/worktree-root.js`; hook não importa `src/`) e é lido por `buildWorktreePath`, pelo mkdir do pai e por `findWorktreeForFeature`; pelo `doctor --worktrees` (`inContainer` e a varredura de resíduo, que olha só `<root>/<repo>-<hash>/` — nunca a pasta de outro repositório, senão o `--prune` apagaria pastas alheias); pelo reaper; pelo SessionStart (`isOutsideContainer`); pelo registro de projetos (a worktree é detectada pelo `.git` ser um arquivo que aponta para `.git/worktrees/`, não pelo nome do diretório pai); e pelos scanners do dashboard. `finish`, `remove` e `list` andam pelo `git worktree list` e não dependem do lugar. **Não faz:** trocar a chave não move as worktrees que já existem.
216
+
217
+ **Por quê.** Um root **acima** da raiz (`..`) é aceito: o contêiner vira um irmão do repositório.
218
+
219
+ ### Claude Code nativo × worktree do morph
220
+
221
+ **Modelo de trabalho: o Claude Code é a porta de entrada, o morph é o dono da worktree** — `claude -w <nome>`, o `EnterWorktree` (por nome) e o `isolation: "worktree"` de sub-agent passam pelo hook `WorktreeCreate` do morph. Segundo a doc do Claude Code, o hook **substitui por inteiro** a criação nativa (o `git worktree add`, o caminho e a branch) e pode devolver **qualquer** caminho na 1ª linha do stdout (o payload traz `name`, `worktree_path` sugerido, `branch_name` e `source_repo_path`; o morph lê `name`). Por isso entram como `worktree setup <f>` (nome de feature conhecido) ou `worktree provision <n>` (o resto), já com a infra, o bloco de portas e o `morph/…`/`morph-task/…`.
222
+
223
+ **O que a criação nativa daria e o morph faz por conta própria, porque o hook a substitui:** `worktree.baseRef` (o hook lê as três camadas de settings), `.worktreeinclude` (a doc: com hook ele "não é processado"; `copyWorktreeIncludes` o honra — o que casa o padrão **e** é ignorado na árvore, nunca `node_modules`/artefato de build/`.git` aninhado, nunca sobrescreve, idempotente; o `provision` continua copiando `.env*` e o resultado sai em `worktreeInclude` no `--json`; **custo medido:** 0,15 s numa raiz com 6 mil arquivos em `node_modules` e 0,5 s (1,4 s a frio) no MORPH_GHLBrain, contra o timeout de 120 s do `WorktreeCreate`, e um pathspec de exclusão não poda a caminhada; **nunca `--exclude-standard`**, que com `--ignored` devolve todo o ignorado — 57 560 caminhos no GHLBrain em vez dos 4 que casam) e a cópia de `.env*`.
224
+
225
+ **Abrir uma worktree que já existe:** abra o `claude` DENTRO dela — uma feature = um worktree = uma instância. `EnterWorktree(path)` para um caminho fora de `.claude/worktrees/` pede aprovação **a cada vez** (regra de permissão e "não perguntar de novo" não a suprimem; só `bypassPermissions`), e uma sessão que entra no meio mantém `CLAUDE_PROJECT_DIR` na raiz e move só o `cwd` do payload: por isso o SessionStart do morph (`worktree-context.js`, `sessionDir`) segue o `cwd` quando ele é um worktree vinculado do MESMO repositório, e a identidade, o bloco de portas e o handoff chegam.
226
+
227
+ **Limpeza:** o sweep periódico nativo mantém qualquer worktree sem o marcador que o Claude grava ("including a worktree a WorktreeCreate hook created"), então **nunca** limpa as do morph; quem limpa é o `worktree reap`, que respeita `git worktree lock` (a marca que o Claude usa em agente rodando) e devolve `locked` como razão. O `WorktreeRemove` (fim de sub-agent e saída de sessão) chama `worktree remove --if-clean`; ele não dispara em `-p` (medido), e é esse o resíduo que o reaper pega.
228
+
229
+ **Isolamento nativo:** numa sessão isolada por `--worktree`/`EnterWorktree` o Claude bloqueia edição, `cwd` de comando e redirect de git (`git -C`, `GIT_DIR`, `cd` para a raiz) para o checkout principal — `finish`, `worktree remove --target` e `worktree reap` rodam da raiz, não de dentro.
230
+
231
+ **Não medido:** se o SessionStart de `claude -w` já chega com o `cwd` no worktree, e o `claude -w "#1234"` (PR): o hook recebe um `name` e cai no `--base` do morph, não no head do PR. Fontes: `code.claude.com/docs/en/worktrees` e `/hooks`.
232
+
233
+ **Por quê.** A remoção nativa não atravessa mais junction no Windows (v2.1.205), mas o `git worktree remove` cru continua atravessando: a regra "só o morph remove" segue valendo.
234
+
235
+ ### Backups do finish e retenção
236
+
237
+ **Backups do finish (`.git/morph-spec/finish-backup/`) e a retenção** — o que o `finish` (o shed da raiz, `{f}-{carimbo}/`) e o desmonte do worktree (`{f}-{carimbo}-worktree-ignored/`) copiam antes de apagar um arquivo que não existe em outro lugar; `worktree remove` de árvore de task grava em `task/{n}-{carimbo}-worktree-ignored/`.
238
+
239
+ Cada pasta ganha um manifesto ao lado (`{pasta}.manifest.json`): o que foi copiado, os caches deixados de fora (`excludedCaches`) e os **nomes** dos `.env*` que entraram (`secrets`). **Um finish é UM backup**: as duas pastas dele levam o mesmo carimbo e contam juntas; pastas gravadas antes disso têm carimbos diferentes e contam uma a uma.
240
+
241
+ **Retenção ligada por padrão**, também em projeto cujo `config.json` não tem a chave: os **20 backups mais novos de cada namespace** (a raiz de `finish-backup/` e `task/` têm orçamentos separados — um fan-out de sub-agents não despeja o backup da feature fechada um minuto antes) **sempre ficam, de qualquer idade** — não há corte por idade no padrão, então um período parado nunca zera os backups das últimas features fechadas —; dentro de um backup que fica, os arquivos `.env*` são apagados depois de **7 dias** (o resto fica, e o manifesto guarda os nomes e `secretsStrippedAt`).
242
+
243
+ Configurável em `config.json → finishBackup.retention: { maxCount, maxAgeDays, secretMaxAgeDays }` (inteiros ≥ 1; `maxAgeDays` é um corte por idade OPCIONAL — ausente ou `null`, o padrão, não corta) ou desligada com `finishBackup.retention: false` (schema em `framework/schemas/config.schema.json`, autoridade em `resolveBackupRetention()`).
244
+
245
+ **O que não dá para ler PULA a poda naquela execução, nunca cai no padrão** — é uma operação que apaga, e um `{ "enabled": false }`, um `"false"`, um `maxCount: 0`, uma chave desconhecida, um `finishBackup` que não é objeto ou um `config.json` que não parseia podem muito bem querer dizer "guarde tudo": a saída diz `backupRetention: {skipped: "<razão>"}`, o `doctor --worktrees --prune` avisa com a razão, e o `doctor`/`doctor --worktrees` acusam até alguém corrigir. Só a ausência da chave liga o padrão.
246
+
247
+ **Backup sem manifesto é legado** (gravado antes desta retenção) — esse é o critério, e nenhum outro. A poda automática nunca o apaga, nunca tira os `.env*` dele e não lhe dá vaga no orçamento; ela só conta quantos esperam (`backupRetention.legacyPending: N` + `legacyHint`). **A primeira poda deles é só com `doctor --worktrees --prune` explícito**, que os ranqueia junto com os demais (contando no teto), apaga os que ficam fora e grava um manifesto (`legacy: true`, `adoptedAt`) em cada um que mantém — dali em diante são backups comuns.
248
+
249
+ A poda automática roda **depois** do backup novo — no fim de todo `finish` com `finished: true` e de todo `worktree remove` que gravou backup (a saída traz `backupRetention: {removed, removedSample, secretsStripped, declined, legacyPending, legacyHint}` só com o que se aplica). **O backup que a execução acabou de gravar nunca é podado por ela** — o carimbo dela é protegido —, e backup com carimbo no FUTURO (relógio adiantado e depois corrigido) fica e não ocupa vaga na contagem.
250
+
251
+ O `finish --dry-run` anuncia só o que a poda AUTOMÁTICA faria (`backupRetention: {outsideRetention, overdueSecrets, legacyPending}` + uma linha em `wouldDo`), sem apagar nada.
252
+
253
+ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o resto do desmonte: backup com reparse point dentro é mantido (`declined`) — mas perde os `.env*`, porque a política o queria fora —, link nunca é atravessado (nem um `task/` que seja junction), e **só `task/` é namespace**: qualquer outra pasta de primeiro nível que não tenha nome de backup nunca é visitada nem removida, nem vazia.
254
+
255
+ **Por quê.** O `.env*` idêntico ao da raiz nunca chega a ser copiado (é replicável, ver acima); o que chega é o que difere — o `.env.e2e` de cada worktree carrega o bloco de portas dela, então nenhum é igual ao da raiz, e é por isso que o prazo curto existe.
256
+
257
+ ### branch prune
258
+
259
+ **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
260
+
261
+ ### handoff
262
+
263
+ **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
264
+
265
+ ### fleet
266
+
267
+ **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
268
+
269
+ ### claim
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 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.
43
272
 
44
273
  ## Validação
45
274
 
46
- - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. UC com nome fora do padrão `UC-{nn}-{slug}.md` (slug com maiúscula ou `_`) continua encontrado, com aviso: o `finish` não o promove ao acervo. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. Os achados de ciclo de vida do `usecase check` (task sobre UC `superseded`, revogação que o `finish` não aplicaria) saem sob `⚠ Ciclo de vida do acervo`, também só aviso, na chave `lifecycle`. **Tamanho de task:** task com mais de 8 `outputs` (`MAX_TASK_OUTPUTS`) é **erro** (exit 1) enquanto o Gate 2 não foi aprovado — fatie por pós-condição — e só aviso depois dele (chore/hotfix nascem com o plano aprovado por política); o `--json` traz `taskSize: {max, oversized, blocking}`. `--mermaid` nunca é bloqueado.
47
- - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **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. **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. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). 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. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo). **Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e o orquestrador repete a chamada.
48
- - **`morph-spec verify <feature> --ci [--json]`** — a prova de escopo de feature tirada do **CI do projeto** (GitHub Actions), não da máquina: faz `git push -u origin morph/<feature>`, espera o run do commit e transforma o `verify --json` que o run publicou (artefato `morph-verify`) no **mesmo stamp** de uma rodada local, com `source: "ci"` e `ciRun` (URL do run); `sha` é o `headSha` do run, então o `verificationStale` compara o código que o CI viu com o HEAD. Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual. Só o **artefato** vira prova, nunca o "verde" do run: workflow de lint, deploy ou de terceiro não compra o Gate 3. **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. **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). 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`. **O runner só tem o que está commitado:** o que testes e e2e leem de arquivo gitignored (`.env`, `appsettings.*.json` local, connection string do Neon, `storageState`) não existe lá, e o run sai vermelho sem defeito de código. Declare o valor como secret do repositório e exponha-o no `env:` do job em `morph-verify.yml` (o arquivo é do projeto: editar à mão é o esperado, e o `findCiWorkflow` continua aceitando enquanto ele chamar `morph-spec verify` publicando `morph-verify.json`), ou desligue com `project.ci: false`.
49
- - **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`). Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `T{n}`, `Gate N`, `spec.md`, 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). 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. 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`. 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ê.
50
- - **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--no-subset] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o avaliador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um UC exige e o avaliador não achou o teste que a morde. **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. **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 avaliador. `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. **Reversão:** bytes do snapshot com md5 conferido, por escrita nova (nunca `os.utime`), rebuild forçado e DLL igual à de antes do 1º mutante. **Exit:** `0` rodou (sobrevivente é achado), `1` a árvore já não compilava, `2` uso, `3` semáforo ocupado, `4` **inseguro** (arquivo não restaurado ou binário que não bate). Segura **um** slot `heavy`.
51
- - **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
52
- - **`morph-spec heavy status [--json]`** e **`morph-spec heavy run [--label <x>] -- <comando…>`** — semáforo de nós pesados **por usuário**: os slots vivem em `%LOCALAPPDATA%morph-specheavy` (Windows) ou `$XDG_STATE_HOME/morph-spec/heavy` (`~/.local/state/…`), compartilhados por **todas** as sessões da máquina, com `heavy.slots` (2) vagas. **Quem toma slot:** build, testes (inclusive `--linux`) e e2e do `verify` — um slot por `verify`, adquirido só imediatamente antes do primeiro spawn real (nó pulado não adquire e a espera fica fora do `durationMs`) —, o `e2e up` (só durante a subida da stack) e o `heavy run`, que roda qualquer comando sob slot e repassa o exit code (`127` se não conseguiu executá-lo; `npm`/`npx`/`pnpm`/`yarn` viram `.cmd` no Windows). `heavy status` é **somente leitura**: quem segura cada slot (pid, label, feature, nó, idade), a limpeza exclusiva do build-server se houver e a RAM livre. **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo de `heavy.minFreeMemGb` (4), espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo). RAM baixa espera **com ou sem** holder de slot: a pressão costuma vir de fora (vmmem do WSL, IDE), e "sem holder → roda" reproduziria o "roda e morre". **Estouro = exit 3 (`busy`), nunca vermelho:** `verify --json` sai `{ status: "busy", persisted: false, busy: { reason, holders, freeGb, waitedMs } }`, **sem stamp e sem evento `Verify`** (nada foi verificado, e fila não é gate verde nem vermelho); `e2e up` e `heavy run` também saem 3. Quem orquestra repete a chamada — nas skills, exit 3 não conta como tentativa e não dispara o loop de correção. **Slot vago:** pid morto, ou idade acima de `heavy.maxHoldMinutes` (90, contra reuso de PID); Ctrl-C deixa o arquivo, que o pid morto cobre. Um filho herda o slot por `MORPH_HEAVY_LEASE` e nunca pede um segundo. **Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado. Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`. **Fim do trabalho pesado:** no fim do `verify` de escopo **feature** (nunca no de task: cada task pagaria compilador frio), no `e2e down` e no `doctor --procs --prune`, roda `dotnet build-server shutdown` **só** se o projeto é .NET, `heavy.shutdownBuildServer` está ligado, nenhum slot está preso **e** nenhum `dotnet build|test|publish|msbuild` está vivo — sub-agents e IDE não passam pelo semáforo, então semáforo vazio não basta. Com `heavy.enabled: false` nunca roda: sem semáforo, nada prova que outra sessão não está compilando. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`. `heavy.disableNodeReuse` põe `MSBUILDDISABLENODEREUSE=1` enquanto o nó pesado roda e restaura depois. O `--linux` rotula o container (`morph-spec.verify=<runId>`, `morph-spec.root=<hash da raiz>`) e o remove **por label** no fim, nunca por nome; o `e2e down` usa `--remove-orphans`. O `verify` **não** derruba a stack e2e (`keepUp` é por desenho). **Bash cru (`heavy.gateBash`, opt-in, padrão `false`):** o `dotnet build`/`dotnet test`/`npm test` de um sub-agent pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda. `heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`. **Não reescreve o comando:** a doc do Claude Code avalia as regras de permissão contra o comando **reescrito**, e o `allow` que acompanha um `updatedInput` aprovaria o que o prompt perguntaria; por isso não há reescritor (o `allow` não atropela regra `deny`/`ask`, mas a reescrita esconderia o comando delas). **O que a vaga não cobre:** ela não tem processo vivo para vigiar (o hook sai na hora), então vence por **TTL** — o `timeout` da chamada, no máximo o da plataforma, mais 30 s — e uma chamada recusada por regra de permissão ou interrompida pelo usuário, que não dispara `Post*`, segura o slot até lá. O TTL conta desde o `PreToolUse`, que roda **antes** do prompt de permissão: se a aprovação demora mais que o `timeout` + 30 s, a vaga vence com o comando ainda por rodar e outra chamada pode tomá-la (sobreassinatura, do lado seguro). Chamadas de Bash de um mesmo lote paralelo rodam **em série** no Claude Code (medido na 2.1.285: o `Pre` da 2ª só rodou depois do `Post` da 1ª), então um agente não fica esperando uma vaga que só a própria chamada anterior liberaria. `run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula 6 dos templates de dispatch (`heavy run`) segue valendo para eles. **Medido** com `claude -p` real e a chain instalada: fila cheia recusa após a espera e não mexe na vaga alheia; a vaga está presa durante a chamada e volta vazia depois, com exit 0 e com exit 1. Custo num Bash **leve**: ~1,5 ms na chain Pre (mediana 61,4 → 62,9 ms, medido intercalado) e ~0 na Post; os módulos do semáforo só carregam quando o comando é pesado. O núcleo (`heavy-config`, `heavy-holders`, `heavy-slots`) mora em `framework/hooks/shared`, porque hook não importa `src/`.
53
- - **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias. Uma linha `.git/morph-spec (<tamanho> · N backup(s) do finish)` mostra o que o morph-spec guarda dentro do `.git` (backups do finish, registros de porta e de claim), que não aparece em `git status`: `✓` normalmente, `⚠` quando há backup legado esperando `--prune`, backup fora da retenção ou `.env*` além do prazo curto (com o comando de poda) — nunca `✗`, e nenhuma linha quando não há nada guardado.
54
- **Personas (`.claude/agents/morph-*.md`).** São artefatos derivados de `.morph/framework/agents.json` — nunca editados à mão. O `doctor` padrão valida o `agents.json` (inclusive o `maxTurns`: inteiro ≥ 1 ou ausente — `0`, negativo, fracionário, string ou `null` é **erro** nomeando persona e valor, porque o installer recusa gerar essa persona — e o `surfaces`: `{ "<superfície>": { when, standards } }` com id kebab-case, `when` e lista não vazios, e nenhum standard repetido da lista universal `standards`; inválido é **erro** pelo mesmo motivo). Com `surfaces`, o corpo da persona ganha `## Standards by surface`: cada superfície com o seu `when`, e a instrução de ler a lista universal mais só a da superfície da tela — a que o checklist de tela do mockup nomeia em `Superfície`. O `--full` compara cada `.md` com o render atual e, no drift, diz **qual campo** divergiu por arquivo — `morph-x.md (maxTurns)` é edição à mão do teto; `(body)` depois de um upgrade da CLI é só regeneração pendente. Os grants `mcp__*` são conferidos contra o registry do **pacote**, o mesmo que o installer leu (a cópia instalada não tem `mcp/`). A linha `tools:` é allowlist estrita: toda persona recebe também `Skill` e `ToolSearch` (sem eles não invoca skill nem carrega MCP adiado), e cada MCP vira um grant por grafia de `serverNames` do registry — o mesmo servidor chama `mcp__context7` no `.mcp.json` e `mcp__plugin_context7_context7` como plugin, e um grant só casa com a própria grafia. O campo `maxTurns` é o teto de turnos do subagent gerado: por persona no `agents.json`, `20` quando ausente, e o corpo da persona o anuncia ao sub-agent junto com o turno de checkpoint (~70%) — o sub-agent não vê o próprio frontmatter. O valor vem do `agents.json` do **pacote**: `update` recopia o arquivo e regenera os `.md`, então editar a cópia em `.morph/framework/` não muda a instalação — só faz o `--full` acusar drift.
55
- **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
56
- **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando; branch `morph/*` cujo diff nunca tocou `.morph/features/{f}/` nem `.morph/archive/{f}/` — um PR só de docs — não é acusada, e diff ilegível ou truncado no teto do `gh` mantém o aviso). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
57
- Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado), e `--ai` para a auditoria de projeto .NET com agentes.
58
- - **`morph-spec doctor --ai`** — auditoria de um projeto **.NET com agentes**, em superfície própria: **não entra no `--full` nem no `doctor` default**. O renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` (não tem o ramo `skip → ○`), então uma checagem de IA lá seria vermelha mesmo dizendo "não verificado"; e cinco linhas `○` permanentes no comando mais rodado do dia é ruído com custo e sem informação. Combinar `--ai` com `--full`/`--mcp`/`--worktrees`/`--procs` é **erro de uso**: exit 1, sem rodar nada — cada superfície tem o seu relatório e o seu código de saída.
59
-
60
- **Cinco checagens.** `pin-drift` (o que o `.csproj` declara × o que `framework/ai-pin.json` fixa), `removed-api` (símbolos renomeados antes do 1.0 GA do `Microsoft.Agents.AI`), `pricing-missing` (todo alias do registry de modelos tem tarifa?), `api-per-alias` (todo alias declara `api`?) e `prompt-source` (as `Instructions` do agente têm fonte declarada?).
61
-
62
- **Só `pin-drift` produz `error`, e só na faixa flutuante** — é a única classe com prejuízo medido: uma faixa `2.*` sobe sozinha para uma versão com assinatura mudada e o `MissingMethodException` chega **depois** de a chamada ao modelo ser paga. Versão fixa ≠ pin é `warn` nomeando as duas versões; pacote de IA presente no projeto e ausente do pin é `warn` descrito como **lacuna do pin**, nunca como defeito do projeto. As outras quatro checagens informam e nunca travam. **Exit code:** 1 com ao menos um `error`; 0 caso contrário — `warn` e `skip` nunca mudam o código de saída.
63
-
64
- **Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
65
- - **`morph-spec doctor --procs [--prune] [--json]`** — o que o morph-spec e as suas sessões deixam de pé na máquina, em superfície própria: **não entra no `--full` nem no default** (mesmo motivo do `--ai`), e combinar com `--full`/`--mcp`/`--worktrees`/`--reset`/`--ai` é **erro de uso**. Sem `--prune` é **somente leitura**. **Processos:** lista `node`, `dotnet`, `testhost`, `esbuild` e Chrome headless de automação (o seu Chrome comum não entra) **sem pai vivo**, com idade e RSS. Órfão = pai ausente da tabela **ou criado depois do filho** (PID reaproveitado). **Nunca é candidato à poda:** processo com menos de 15 min ou de idade desconhecida; linha de comando que casa com **qualquer MCP configurado** (`.mcp.json`, `~/.claude.json` do projeto e do usuário, `.mcp.json` dos plugins habilitados), mesmo órfão, porque no Windows a cadeia `claude→cmd→npx→node` perde o elo quando o `cmd` do meio morre; o worker de refresh do grafo; e os nós de build (MSBuild `/nodemode`, VBCSCompiler), compartilhados entre sessões — esses saem por `dotnet build-server shutdown` quando ociosos, nunca por kill. `--prune` encerra **por PID** só o que sobrou, relendo o início e a linha de comando daquele PID imediatamente antes (PID reaproveitado é pulado); **nunca por nome de imagem** (`taskkill /IM node.exe` derrubaria os MCPs de todas as sessões). Exit 1 só se algum encerramento falhar. **MCP duplicado:** o mesmo servidor no `.mcp.json`/`~/.claude.json` e entregue por plugin habilitado sobe dois processos (`mcp__X` e `mcp__plugin_X_X`); o relatório só sugere remover do `.mcp.json` ou desligar o plugin. O `init` e o `mcp setup --auto` deixaram de recriar a duplicata: um plugin habilitado que entrega o servidor conta como instalado. **WSL:** `%USERPROFILE%\.wslconfig` ausente, ou sem `memory=` (seção `[wsl2]`) ou sem `autoMemoryReclaim=` (seção `[experimental]`), vira sugestão com o trecho e o `wsl --shutdown`; o morph-spec **nunca escreve fora do repositório**. O `--json` traz `procs`, `mcp`, `wsl` e, com `--prune`, `prune`. Listagem indisponível (PowerShell falhou, plataforma sem listador) sai **não verificado** com razão, nunca lista vazia.
66
- - **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`). Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível. **Sobras de sub-agent:** achado `agent-worktree-leftover` (`info`, nunca falha): `reapable` (fix: `morph-spec worktree reap`) ou preservada com a razão (`dirty`, `exclusive-commits`, `no-usage-event`, `directory-missing`, `unknown`); agente ativo e agente dentro da graça não geram achado, e a árvore não é reportada de novo como `unintegrated-work`. O `--prune` também roda o `worktree reap` (`prune.agentReap` no `--json`).
67
- - **`morph-spec report-check <feature> [task...] [--before-eval] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache; relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns` — sai `report-incomplete` com `partial: true` e ação `resume`: retome o agente, NÃO carimbe DEGRADADO. O marcador é lido de propósito, nunca inferido da falta de um item). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Vários ids (`report-check {f} T1 T2 --json`) auditam um batch paralelo numa chamada: sem task, o modo feature só audita tasks `done`, e as de um batch recém-voltado ainda não estão. **Bloco `tree`**, em toda saída e sem mudar veredito nem exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `markers` (linhas com `MUTACAO` em arquivo rastreado ou não, fora dos ignorados), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde (fora de repositório ou erro) — desconhecido, nunca árvore limpa. **`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch do avaliador, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`: os arquivos cujo conteúdo não é o da foto, inclusive o que foi commitado desde então. É assim que uma sessão que retoma depois de o avaliador morrer no meio vê a mutação que ficou; sem foto, `sinceBeforeEval` é `null`. É o que o orquestrador lê depois de um dispatch, em vez de `git status`/`git diff`/`git log`/`grep`, e o que cola no recado de retomada. Relatório completo = prosa do que foi feito, uma lista de **Arquivos:** e os **Critérios:** (cada `doneCriteria` com a evidência). Com `--before-eval`, a saída traz também `evaluatorModel` (`sonnet` por padrão, `opus` quando o diff toca auth, segredo, pagamento, migração, prompt/modelo ou mídia gerada, com `reasons`). Protocolo completo em MORPH.md §6.
68
- - **`morph-spec scout-check <mapa> [--root <dir>] [--json]`** — audita o mapa do `morph-scout` antes de o plano usá-lo. Para cada `caminho:linha` ou `caminho:l1-l2` (em crase ou solto; contrabarra Windows e absoluto aceitos): o arquivo existe sob a raiz (`--root`, senão o topo da worktree corrente — nunca a raiz primária, cujo conteúdo é outro), as linhas existem, e o trecho em crase que segue a citação na mesma linha aparece nelas com tolerância de ±2 linhas (`...` separa fragmentos que têm de aparecer em ordem). Só o nome do arquivo vale quando é único na árvore (`git ls-files`, rastreado ou não); ambíguo reprova listando os candidatos. Linha `caminho:N:conteúdo` num bloco cercado (saída de grep) é conferida contra a linha N. Veredito de ausência ou de vida (VIVO/MORTO, "sem consumidor", "nenhum teste", "ninguém escreve"…) só passa sob um heading Inferido/Hipótese (ou com o marcador na linha), com o comando de busca em crase e a contagem bruta; o checker NÃO reconta a busca — o orquestrador confere com grep próprio o que muda o plano. Mapa sem nenhuma citação reprova (`no-citations`). Falhas: `missing-file`, `ambiguous-path`, `line-out-of-range`, `missing-quote`, `quote-mismatch`, `verdict-outside-inference`, `verdict-without-command`, `verdict-without-count`, `no-citations`. Sai 0 só sem falha, 1 com falha, 2 com o mapa ausente. Caminho canônico do mapa: `.morph/features/{f}/2-plan/scout-map.md`. Protocolo em MORPH.md §6.
275
+ ### verify
276
+
277
+ **`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira.
278
+
279
+ O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build).
280
+
281
+ **Onde o nó `build` compila** (chaves de `project` em `.morph/config/config.json`, forma em `config.schema.json`): `project.stack` (tags unidas por `+`, ex. `dotnet+nextjs`) escolhe os passos; `project.backendPath` é a pasta da solução .NET e `project.frontendPath` a do frontend (`tsc`), ambas relativas à raiz. Sem `backendPath` e sem `.sln`/`.slnx`/`.csproj` na raiz, o build compila a **única** `.sln`/`.slnx` do repositório (senão o único `.csproj`), fora de `worktrees/`, `node_modules/`, `bin/` e `obj/` (`discoverDotnetSolution`, `test-runner.js`); nenhuma ou várias → o build falha e o `feedback[]` lista as candidatas e manda declarar `project.backendPath`.
282
+
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
+
285
+ **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
+
287
+ **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.
288
+
289
+ **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo.
290
+
291
+ **`--filter <ids>` escolhe os nós** (lista separada por vírgula, casando `id` exato ou substring). O Gate 3 é `verify {f} --json` **sem filtro**, porque a prova que o verificador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar.
292
+
293
+ **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não.
294
+
295
+ **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado.
296
+
297
+ **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval da `morph-apply` §6 (cenário `prova: eval`) é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`.
298
+
299
+ **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar).
300
+
301
+ **Rodada repetida:** se nada mudou fora de `.morph/` desde o verify anterior (a árvore commitada, diff, arquivos não rastreados e o `config.json` sem `expectedCount` — um commit só de `.morph/` não conta), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não.
302
+
303
+ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo.
304
+
305
+ **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
306
+
307
+ **Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e o orquestrador repete a chamada.
308
+
309
+ **Por quê.** Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar.
310
+
311
+ ### verify --ci
312
+
313
+ **`morph-spec verify <feature> --ci [--json]`** — a prova de escopo de feature tirada do **CI do projeto** (GitHub Actions), não da máquina: faz `git push -u origin morph/<feature>`, espera o run do commit e transforma o `verify --json` que o run publicou (artefato `morph-verify`) no **mesmo stamp** de uma rodada local, com `source: "ci"` e `ciRun` (URL do run); `sha` é o `headSha` do run, então o `verificationStale` compara o código que o CI viu com o HEAD.
314
+
315
+ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
316
+
317
+ **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
+
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.
320
+
321
+ 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
+
323
+ **O runner só tem o que está commitado:** o que testes e e2e leem de arquivo gitignored (`.env`, `appsettings.*.json` local, connection string do Neon, `storageState`) não existe lá, e o run sai vermelho sem defeito de código. Declare o valor como secret do repositório e exponha-o no `env:` do job em `morph-verify.yml` (o arquivo é do projeto: editar à mão é o esperado, e o `findCiWorkflow` continua aceitando enquanto ele chamar `morph-spec verify` publicando `morph-verify.json`), ou desligue com `project.ci: false`.
324
+
325
+ **Por quê.** Só o **artefato** vira prova, nunca o "verde" do run: workflow de lint, deploy ou de terceiro não compra o Gate 3.
326
+
327
+ ### Validador code-budget
328
+
329
+ **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
+
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).
332
+
333
+ 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
+
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`.
336
+
337
+ **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
+
339
+ ### mutate
340
+
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.
342
+
343
+ **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
+
345
+ **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
+
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.
348
+
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.
350
+
351
+ **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
+
353
+ **Reversão:** bytes do snapshot com md5 conferido, por escrita nova (nunca `os.utime`), rebuild forçado e DLL igual à de antes do 1º mutante.
354
+
355
+ **Exit:** `0` rodou (sobrevivente é achado), `1` a árvore já não compilava, `2` uso, `3` semáforo ocupado, `4` **inseguro** (arquivo não restaurado ou binário que não bate). Segura **um** slot `heavy`.
356
+
357
+ ### e2e
358
+
359
+ **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo.
360
+
361
+ `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed.
362
+
363
+ `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do verificador, em vez de deixá-lo adivinhar `localhost:3000`.
364
+
365
+ `down [--volumes]` é best-effort e nunca bloqueia.
366
+
367
+ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada.
368
+
369
+ **Por quê.** Na dúvida, rebuild — o layer cache do Docker faz disso barato. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
370
+
371
+ ### heavy
372
+
373
+ **`morph-spec heavy status [--json]`** e **`morph-spec heavy run [--label <x>] -- <comando…>`** — semáforo de nós pesados **por usuário**: os slots vivem em `%LOCALAPPDATA%morph-specheavy` (Windows) ou `$XDG_STATE_HOME/morph-spec/heavy` (`~/.local/state/…`), compartilhados por **todas** as sessões da máquina, com `heavy.slots` (2) vagas.
374
+
375
+ **Quem toma slot:** build, testes (inclusive `--linux`) e e2e do `verify` — um slot por `verify`, adquirido só imediatamente antes do primeiro spawn real (nó pulado não adquire e a espera fica fora do `durationMs`) —, o `e2e up` (só durante a subida da stack) e o `heavy run`, que roda qualquer comando sob slot e repassa o exit code (`127` se não conseguiu executá-lo; `npm`/`npx`/`pnpm`/`yarn` viram `.cmd` no Windows).
376
+
377
+ `heavy status` é **somente leitura**: quem segura cada slot (pid, label, feature, nó, idade), a limpeza exclusiva do build-server se houver e a RAM livre.
378
+
379
+ **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo de `heavy.minFreeMemGb` (4), espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo).
380
+
381
+ **Estouro = exit 3 (`busy`), nunca vermelho:** `verify --json` sai `{ status: "busy", persisted: false, busy: { reason, holders, freeGb, waitedMs } }`, **sem stamp e sem evento `Verify`** (nada foi verificado, e fila não é gate verde nem vermelho); `e2e up` e `heavy run` também saem 3. Quem orquestra repete a chamada — nas skills, exit 3 não conta como tentativa e não dispara o loop de correção.
382
+
383
+ **Slot vago:** pid morto, ou idade acima de `heavy.maxHoldMinutes` (90, contra reuso de PID); Ctrl-C deixa o arquivo, que o pid morto cobre. Um filho herda o slot por `MORPH_HEAVY_LEASE` e nunca pede um segundo.
384
+
385
+ **Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado.
386
+
387
+ Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`.
388
+
389
+ **Fim do trabalho pesado:** no fim do `verify` de escopo **feature** (nunca no de task: cada task pagaria compilador frio), no `e2e down` e no `doctor --procs --prune`, roda `dotnet build-server shutdown` **só** se o projeto é .NET, `heavy.shutdownBuildServer` está ligado, nenhum slot está preso **e** nenhum `dotnet build|test|publish|msbuild` está vivo — sub-agents e IDE não passam pelo semáforo, então semáforo vazio não basta. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`.
390
+
391
+ `heavy.disableNodeReuse` põe `MSBUILDDISABLENODEREUSE=1` enquanto o nó pesado roda e restaura depois. O `--linux` rotula o container (`morph-spec.verify=<runId>`, `morph-spec.root=<hash da raiz>`) e o remove **por label** no fim, nunca por nome; o `e2e down` usa `--remove-orphans`. O `verify` **não** derruba a stack e2e (`keepUp` é por desenho).
392
+
393
+ **Bash cru (`heavy.gateBash`, opt-in, padrão `false`):** o `dotnet build`/`dotnet test`/`npm test` de um sub-agent pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda.
394
+
395
+ `heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`.
396
+
397
+ **Não reescreve o comando:** a doc do Claude Code avalia as regras de permissão contra o comando **reescrito**, e o `allow` que acompanha um `updatedInput` aprovaria o que o prompt perguntaria; por isso não há reescritor (o `allow` não atropela regra `deny`/`ask`, mas a reescrita esconderia o comando delas).
398
+
399
+ **O que a vaga não cobre:** ela não tem processo vivo para vigiar (o hook sai na hora), então vence por **TTL** — o `timeout` da chamada, no máximo o da plataforma, mais 30 s — e uma chamada recusada por regra de permissão ou interrompida pelo usuário, que não dispara `Post*`, segura o slot até lá. O TTL conta desde o `PreToolUse`, que roda **antes** do prompt de permissão: se a aprovação demora mais que o `timeout` + 30 s, a vaga vence com o comando ainda por rodar e outra chamada pode tomá-la (sobreassinatura, do lado seguro). Chamadas de Bash de um mesmo lote paralelo rodam **em série** no Claude Code (medido na 2.1.285: o `Pre` da 2ª só rodou depois do `Post` da 1ª), então um agente não fica esperando uma vaga que só a própria chamada anterior liberaria.
400
+
401
+ `run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula 6 dos templates de dispatch (`heavy run`) segue valendo para eles.
402
+
403
+ **Medido** com `claude -p` real e a chain instalada: fila cheia recusa após a espera e não mexe na vaga alheia; a vaga está presa durante a chamada e volta vazia depois, com exit 0 e com exit 1. Custo num Bash **leve**: ~1,5 ms na chain Pre (mediana 61,4 → 62,9 ms, medido intercalado) e ~0 na Post; os módulos do semáforo só carregam quando o comando é pesado.
404
+
405
+ **Por quê.** RAM baixa espera **com ou sem** holder de slot: a pressão costuma vir de fora (vmmem do WSL, IDE), e "sem holder → roda" reproduziria o "roda e morre". Com `heavy.enabled: false` nunca roda: sem semáforo, nada prova que outra sessão não está compilando. O núcleo (`heavy-config`, `heavy-holders`, `heavy-slots`) mora em `framework/hooks/shared`, porque hook não importa `src/`.
406
+
407
+ ### doctor
408
+
409
+ **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias. Uma linha `.git/morph-spec (<tamanho> · N backup(s) do finish)` mostra o que o morph-spec guarda dentro do `.git` (backups do finish, registros de porta e de claim), que não aparece em `git status`: `✓` normalmente, `⚠` quando há backup legado esperando `--prune`, backup fora da retenção ou `.env*` além do prazo curto (com o comando de poda) — nunca `✗`, e nenhuma linha quando não há nada guardado.
410
+
411
+ **Personas (`.claude/agents/morph-*.md`).** São artefatos derivados de `.morph/framework/agents.json` — nunca editados à mão. O `doctor` padrão valida o `agents.json` (inclusive o `maxTurns`: inteiro ≥ 1 ou ausente — `0`, negativo, fracionário, string ou `null` é **erro** nomeando persona e valor, porque o installer recusa gerar essa persona — e o `surfaces`: `{ "<superfície>": { when, standards } }` com id kebab-case, `when` e lista não vazios, e nenhum standard repetido da lista universal `standards`; inválido é **erro** pelo mesmo motivo). Com `surfaces`, o corpo da persona ganha `## Standards by surface`: cada superfície com o seu `when`, e a instrução de ler a lista universal mais só a da superfície da tela — a que o checklist de tela do mockup nomeia em `Superfície`. O `--full` compara cada `.md` com o render atual e, no drift, diz **qual campo** divergiu por arquivo — `morph-x.md (maxTurns)` é edição à mão do teto; `(body)` depois de um upgrade da CLI é só regeneração pendente. Os grants `mcp__*` são conferidos contra o registry do **pacote**, o mesmo que o installer leu (a cópia instalada não tem `mcp/`). A linha `tools:` é allowlist estrita: toda persona recebe também `Skill` e `ToolSearch` (sem eles não invoca skill nem carrega MCP adiado), e cada MCP vira um grant por grafia de `serverNames` do registry — o mesmo servidor chama `mcp__context7` no `.mcp.json` e `mcp__plugin_context7_context7` como plugin, e um grant só casa com a própria grafia. O campo `maxTurns` é o teto de turnos do subagent gerado: por persona no `agents.json`, `20` quando ausente, e o corpo da persona o anuncia ao sub-agent junto com o turno de checkpoint (~70%) — o sub-agent não vê o próprio frontmatter. O valor vem do `agents.json` do **pacote**: `update` recopia o arquivo e regenera os `.md`, então editar a cópia em `.morph/framework/` não muda a instalação — só faz o `--full` acusar drift.
412
+
413
+ **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
414
+
415
+ **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando; branch `morph/*` cujo diff nunca tocou `.morph/features/{f}/` nem `.morph/archive/{f}/` — um PR só de docs — não é acusada, e diff ilegível ou truncado no teto do `gh` mantém o aviso). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
416
+
417
+ Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado), e `--ai` para a auditoria de projeto .NET com agentes.
418
+
419
+ **Por quê.** As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê.
420
+
421
+ ### doctor --ai
422
+
423
+ **`morph-spec doctor --ai`** — auditoria de um projeto **.NET com agentes**, em superfície própria: **não entra no `--full` nem no `doctor` default**. Combinar `--ai` com `--full`/`--mcp`/`--worktrees`/`--procs` é **erro de uso**: exit 1, sem rodar nada — cada superfície tem o seu relatório e o seu código de saída.
424
+
425
+ **Cinco checagens.** `pin-drift` (o que o `.csproj` declara × o que `framework/ai-pin.json` fixa), `removed-api` (símbolos renomeados antes do 1.0 GA do `Microsoft.Agents.AI`), `pricing-missing` (todo alias do registry de modelos tem tarifa?), `api-per-alias` (todo alias declara `api`?) e `prompt-source` (as `Instructions` do agente têm fonte declarada?).
426
+
427
+ **Só `pin-drift` produz `error`, e só na faixa flutuante** — é a única classe com prejuízo medido: uma faixa `2.*` sobe sozinha para uma versão com assinatura mudada e o `MissingMethodException` chega **depois** de a chamada ao modelo ser paga. Versão fixa ≠ pin é `warn` nomeando as duas versões; pacote de IA presente no projeto e ausente do pin é `warn` descrito como **lacuna do pin**, nunca como defeito do projeto. As outras quatro checagens informam e nunca travam. **Exit code:** 1 com ao menos um `error`; 0 caso contrário — `warn` e `skip` nunca mudam o código de saída.
428
+
429
+ **Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
430
+
431
+ **Por quê.** O renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` (não tem o ramo `skip → ○`), então uma checagem de IA lá seria vermelha mesmo dizendo "não verificado"; e cinco linhas `○` permanentes no comando mais rodado do dia é ruído com custo e sem informação.
432
+
433
+ ### doctor --procs
434
+
435
+ **`morph-spec doctor --procs [--prune] [--json]`** — o que o morph-spec e as suas sessões deixam de pé na máquina, em superfície própria: **não entra no `--full` nem no default** (mesmo motivo do `--ai`), e combinar com `--full`/`--mcp`/`--worktrees`/`--reset`/`--ai` é **erro de uso**. Sem `--prune` é **somente leitura**.
436
+
437
+ **Processos:** lista `node`, `dotnet`, `testhost`, `esbuild` e Chrome headless de automação (o seu Chrome comum não entra) **sem pai vivo**, com idade e RSS. Órfão = pai ausente da tabela **ou criado depois do filho** (PID reaproveitado).
438
+
439
+ **Nunca é candidato à poda:** processo com menos de 15 min ou de idade desconhecida; linha de comando que casa com **qualquer MCP configurado** (`.mcp.json`, `~/.claude.json` do projeto e do usuário, `.mcp.json` dos plugins habilitados), mesmo órfão, porque no Windows a cadeia `claude→cmd→npx→node` perde o elo quando o `cmd` do meio morre; o worker de refresh do grafo; e os nós de build (MSBuild `/nodemode`, VBCSCompiler), compartilhados entre sessões — esses saem por `dotnet build-server shutdown` quando ociosos, nunca por kill.
440
+
441
+ `--prune` encerra **por PID** só o que sobrou, relendo o início e a linha de comando daquele PID imediatamente antes (PID reaproveitado é pulado); **nunca por nome de imagem** (`taskkill /IM node.exe` derrubaria os MCPs de todas as sessões). Exit 1 só se algum encerramento falhar.
442
+
443
+ **MCP duplicado:** o mesmo servidor no `.mcp.json`/`~/.claude.json` e entregue por plugin habilitado sobe dois processos (`mcp__X` e `mcp__plugin_X_X`); o relatório só sugere remover do `.mcp.json` ou desligar o plugin. O `init` e o `mcp setup --auto` deixaram de recriar a duplicata: um plugin habilitado que entrega o servidor conta como instalado.
444
+
445
+ **WSL:** `%USERPROFILE%\.wslconfig` ausente, ou sem `memory=` (seção `[wsl2]`) ou sem `autoMemoryReclaim=` (seção `[experimental]`), vira sugestão com o trecho e o `wsl --shutdown`; o morph-spec **nunca escreve fora do repositório**.
446
+
447
+ O `--json` traz `procs`, `mcp`, `wsl` e, com `--prune`, `prune`. Listagem indisponível (PowerShell falhou, plataforma sem listador) sai **não verificado** com razão, nunca lista vazia.
448
+
449
+ ### doctor --worktrees
450
+
451
+ **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas.
452
+
453
+ `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run).
454
+
455
+ O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`).
456
+
457
+ Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado.
458
+
459
+ **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar.
460
+
461
+ **A quarta fonte não é um arquivo: as stacks docker.** Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura.
462
+
463
+ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível.
464
+
465
+ **Sobras de sub-agent:** achado `agent-worktree-leftover` (`info`, nunca falha): `reapable` (fix: `morph-spec worktree reap`) ou preservada com a razão (`dirty`, `exclusive-commits`, `no-usage-event`, `directory-missing`, `unknown`); agente ativo e agente dentro da graça não geram achado, e a árvore não é reportada de novo como `unintegrated-work`. O `--prune` também roda o `worktree reap` (`prune.agentReap` no `--json`).
466
+
467
+ **Por quê.** Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas.
468
+
469
+ ### report-check
470
+
471
+ **`morph-spec report-check <feature> [F1 F2 ...] [--before-eval] [--json]`** — audita o relatório de cada fatia (`3-implement/reports/F{n}.md`) contra o trabalho que realmente entrou.
472
+
473
+ Vereditos: `ok`; `report-stale` → atualize a partir de `observed`, NÃO re-despache; `report-incomplete` com `partial: true` (relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns`) e ação `resume`: retome o mesmo agente, NÃO carimbe DEGRADADO; `report-incomplete` sem marcador → `complete-degraded`; `work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-missing-evidence-unknown` quando não há telemetria. O marcador é lido de propósito, nunca inferido da falta de um item.
474
+
475
+ **Evidência por agente:** `observed` vem do `events.jsonl` de toda árvore — o dono da fatia é o `agentId` do último `Write` do relatório, e contam os writes desse agente; sem `agentId` (relatório inline do orquestrador) contam os writes aos `arquivos:` da fatia. Writes do orquestrador sob `.morph/` não contam. Sem fatia na linha de comando, audita toda fatia do `checks.json`.
476
+
477
+ Relatório completo = prosa do que foi feito, uma linha `**Arquivos:**` e cada cenário `C{n}` que a fatia cobre citado pelo id — a mesma régua (`inspectReport`, em `hooks/shared/slice-progress.js`) que o Stop hook aplica.
478
+
479
+ **Bloco `tree`**, em toda saída e sem mudar veredito nem exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `markers` (linhas com `MUTACAO` em arquivo rastreado ou não, fora dos ignorados), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde — desconhecido, nunca árvore limpa.
480
+
481
+ **`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. É o que o orquestrador lê depois de um dispatch, em vez de `git status`/`git diff`/`git log`/`grep`.
482
+
483
+ Sem fatia na linha de comando, a saída traz `reviews: { verifier, reviewer, security }`, cada um `{ exists, complete, partial, verdict }` (`complete` = veredito legível e sem `PARCIAL` na 1ª linha). Com `--before-eval`, a saída traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou fatia com standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`. A ação de cada veredito sai no campo `hint`; a tabela veredito → ação está na `morph-apply` §5.
484
+
485
+ **Por quê.** **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho do mesmo agente — completo e mentindo.
486
+
487
+ ### scout-check
488
+
489
+ **`morph-spec scout-check <mapa> [--root <dir>] [--json]`** — audita o mapa do `morph-scout` antes de o plano usá-lo. Para cada `caminho:linha` ou `caminho:l1-l2` (em crase ou solto; contrabarra Windows e absoluto aceitos): o arquivo existe sob a raiz (`--root`, senão o topo da worktree corrente — nunca a raiz primária, cujo conteúdo é outro), as linhas existem, e o trecho em crase que segue a citação na mesma linha aparece nelas com tolerância de ±2 linhas (`...` separa fragmentos que têm de aparecer em ordem). Só o nome do arquivo vale quando é único na árvore (`git ls-files`, rastreado ou não); ambíguo reprova listando os candidatos. Linha `caminho:N:conteúdo` num bloco cercado (saída de grep) é conferida contra a linha N. Veredito de ausência ou de vida (VIVO/MORTO, "sem consumidor", "nenhum teste", "ninguém escreve"…) só passa sob um heading Inferido/Hipótese (ou com o marcador na linha), com o comando de busca em crase e a contagem bruta; o checker NÃO reconta a busca — o orquestrador confere com grep próprio o que muda o plano. Mapa sem nenhuma citação reprova (`no-citations`). Falhas: `missing-file`, `ambiguous-path`, `line-out-of-range`, `missing-quote`, `quote-mismatch`, `verdict-outside-inference`, `verdict-without-command`, `verdict-without-count`, `no-citations`. Sai 0 só sem falha, 1 com falha, 2 com o mapa ausente. Caminho canônico do mapa: `.morph/features/{f}/2-plan/scout-map.md`. Protocolo em MORPH.md §6.
490
+
491
+ ### plan-check
492
+
493
+ **`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto em `2-plan/plan.md`; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão). **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale —, `Fatias`); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual`; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`F`/`ADR` duplicado; fatia sem `persona:` ou com persona fora do `agents.json`; fatia com mais de 8 `arquivos:`; `cobre:` de cenário inexistente; cenário que nenhuma fatia cobre; `depende:` inexistente ou em ciclo; veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`. **Avisos:** `CRIAR` sem `why`, fatia sem `cobre:`, chave de fatia desconhecida, `superfície` fora das `surfaces` da `ui-designer`, ADR sem `**Alcance:** feature|projeto`, mais de 12 fatias. Com o gate `plan` aprovado e o corpo mudado fora da `## Errata`, recusa (`frozen`, exit 1) — o `planHash` ignora a errata; mudança de escopo é `unapprove <f> plan`, `plan-check` e o gate de novo. Sem erro, imprime as ondas paralelas (fatias sem `depende:` comum e sem arquivo em comum, até 4 por onda). Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a.
494
+
495
+ ### stack-scan
496
+
497
+ **`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.
69
498
 
70
499
  ## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
71
500
 
@@ -83,8 +512,8 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
83
512
  "build": ["tests/App.Tests/App.Tests.csproj"], "expectedCount": 3057, "timeoutMs": 600000 },
84
513
  { "id": "vitest", "runner": "vitest", "cwd": "catalog-admin",
85
514
  "command": "npx vitest run --reporter=default", "expectedCount": 3082 },
86
- { "id": "playwright", "runner": "playwright", "cwd": "catalog-admin",
87
- "command": "npx playwright test", "e2e": true }
515
+ { "id": "api-integration", "runner": "dotnet", "cwd": ".",
516
+ "command": "dotnet test tests/App.Integration.Tests/App.Integration.Tests.csproj -v q", "e2e": true }
88
517
  ]
89
518
  }
90
519
  }
@@ -93,16 +522,14 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
93
522
  | Campo | Obrigatório | Semântica |
94
523
  | --- | --- | --- |
95
524
  | `id` | **sim** | `^[a-z0-9][a-z0-9-]*$`, único no array. É o que `--filter` casa e a chave de `runs[]`. `id` duplicado é erro de config, não empate silencioso — **os dois** lados falham |
96
- | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify — **com uma exceção:** o recorte do escopo de task (*O recorte por task*, abaixo) estreita o comando e o registra: `runs[].command` é o que rodou, `runs[].selection.declaredCommand` o declarado |
525
+ | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
97
526
  | `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
98
527
  | `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
99
528
  | `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
100
529
  | `timeoutMs` | não | Teto deste nó. Existe porque o default derivado (120 s × projetos encadeados) é desenhado para `dotnet test` e é hostil a uma suíte Node de doze minutos |
101
- | `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
102
- | `e2e` | não (`false`) | Exige a stack de pé. Roda em escopo de feature; é pulado **com motivo** no loop por task e dentro do `--linux` |
530
+ | `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). O `mutate` não o exige: sem ele, descobre o alvo pelo comando. Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
531
+ | `e2e` | não (`false`) | Exige a stack de pé. No host roda **depois** do nó `e2e`, contra a stack que ele subiu e com o mesmo env do compose (portas, `PLAYWRIGHT_BASE_URL`, `COMPOSE_PROJECT_NAME`); stack que não subiu (`e2e` em `skip`/`fail`) pula o nó com o motivo e um aviso `[tests/{id}] warning:`. Pulado **com motivo** dentro do `--linux`. Playwright não entra aqui: é o próprio nó `e2e` (detectado ou `e2e.command`), e declará-lo também como nó de teste rodaria a suíte duas vezes |
103
532
  | `linuxImage` | não | Imagem do container deste nó sob `--linux` |
104
- | `groups` | não (default pelo `runner`) | A que `group` de task o nó responde no recorte por task: subconjunto não vazio, sem repetição, de `backend` \| `frontend` \| `tests` \| `infra` \| `docs` (o mesmo enum do `group` de `tasks.json`). Ausente: `dotnet`/`node-test` → `backend`; `vitest`/`jest`/`playwright` → `frontend`; `none`/`evals` não têm default — nó sem grupo nunca sai do escopo pelo `group`. Valor inválido é erro de config (`invalid-test-node`), como nos demais campos |
105
-
106
533
 
107
534
  ### O runner `evals`: regressão de prompt e de modelo com cache commitada
108
535
 
@@ -161,10 +588,6 @@ Pagar o relógio de uma suíte inteira para descobrir no fim que a cache não se
161
588
  possível. E um `skip` aqui seria gate verde sobre verificação que não aconteceu — o defeito que a
162
589
  lista plural de nós existe para matar.
163
590
 
164
- **Escopo.** O nó de eval **não roda no `verify` por task**: é pulado com
165
- `eval node runs at feature scope only`, o mesmo tratamento de `e2e: true`. O relógio de uma task não
166
- paga uma suíte de eval, e o resultado que o Gate 3 lê é o de escopo de feature.
167
-
168
591
  **Contagem.** A contagem do nó é o `scenarios` do sumário — e só ele: sem sumário legível ela é
169
592
  **`null`, DESCONHECIDA, jamais zero**, e um sumário ilegível não pode fabricar uma queda contra
170
593
  `expectedCount` nem virar baseline. E um sumário dizendo
@@ -257,12 +680,10 @@ atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **qu
257
680
  vermelha, execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
258
681
  afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
259
682
  `project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
260
- fallback sintetiza exigiria inventar a declaração que o comando veio manter). E, antes das quatro, `{task}` na
261
- linha de comando é erro de uso (exit 2): baseline é da frota inteira, e o escopo de task recorta nós e testes. Verde com gravação
683
+ fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
262
684
  recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
263
685
  pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
264
- testes rodaram. A execução que **gravou** a baseline (verde e inteira) persiste o stamp `verification` como qualquer verify de
265
- feature: ela mediu a suíte toda, e rodá-la de novo só para ter prova seria pagar duas vezes pelo mesmo
686
+ 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
266
687
  verde. Sem gravação (vermelha, parcial, contagem desconhecida) não há stamp, e o `reason` do `--json`
267
688
  nomeia o nó que impediu. **Consequência a conhecer:** um `expectedCount` digitado à
268
689
  mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
@@ -275,218 +696,54 @@ passa com aviso de alta, e o comando grava.
275
696
  | Momento | Comando | Por quê |
276
697
  | --- | --- | --- |
277
698
  | Sub-agent implementador | só o filtro dos próprios testes, **uma vez**, antes do relatório (nunca `morph-spec verify`, nunca red→green) | A prova é do orquestrador; cada rodada extra dentro do sub-agent custa turnos que o teto corta |
278
- | Loop por task | `verify {f} {task}` (recorte automático: o `group` escolhe os nós, os `outputs` estreitam os testes — ver *O recorte por task*; `--filter {stack-da-task}` só para forçar os nós, `--full` para desligar) | O relógio de uma task não paga a suíte inteira; a task tocou **um** stack |
279
699
  | Gate 3 | `verify {f} --ci --json` (sem filtro; sem CI disponível, roda local) | A prova tem de cobrir **todos** os nós; no CI ela não ocupa a máquina e roda em paralelo com o avaliador |
280
700
  | CI | a suíte inteira, todos os nós (`morph-verify.yml` roda o mesmo `verify {f}`) | idem — e o artefato dele é o stamp do Gate 3 |
281
- | Evidência de eval (task com `evidencias/` em `outputs`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals` (pulado no escopo de task), sem stack e sem carimbo — não é a verificação da task, é a prova que o `morph-implement` §5 grava |
701
+ | Evidência de eval (cenário `Cn` com `prova: eval`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals`, sem stack e sem carimbo — é a evidência de runtime do cenário, não a verificação da feature |
282
702
 
283
703
  > **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
284
704
  > corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
285
705
  > relógios que o CLI de fato tem: build **120 000 ms**, tests **120 000 ms por projeto de teste
286
706
  > encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
287
707
  > que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
288
- > **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** no loop, o
289
- > recorte por task (ou `--filter`) a evita; no Gate 3, rode `verify {feature}` com `run_in_background` e
708
+ > **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
709
+ > `verify {feature}` com `run_in_background` e
290
710
  > espere a notificação de término — nunca `sleep`/`until` consultando o processo.
291
711
  > Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum
292
712
  > teste falhou`) em vez de devolver log truncado.
293
713
 
294
- ### O recorte por task: `verify {f} {task}`
295
-
296
- Sem task, o `verify` roda tudo e **nunca recorta** — é a prova do Gate 3. Com `{task}`, ele lê de
297
- `tasks.json` o `group` e os `outputs` da task e toma duas decisões independentes:
298
-
299
- 1. **Quais nós.** O `group` da task escolhe os nós pelo campo `groups` de cada um (ou pelo default do
300
- `runner`, na tabela de campos acima). Um nó que serve outro grupo sai do escopo e **aparece** em
301
- `runs[]` como `skip` com o motivo (`out of task scope: …`) — a menos que seja dono de um arquivo de
302
- teste da task (rede para task mal agrupada) ou não tenha grupo algum. Grupo que nenhum nó serve (por
303
- default, `tests` e `infra`) ou task sem `group` → todos os nós. Task `docs` roda só os nós donos de
304
- algum arquivo dos `outputs` — dentro do `cwd` do nó, ou compilado pelo `.csproj` de um link do
305
- comando; um nó de `cwd` `.` é dono de tudo, então no layout de nó único a task `docs` roda o nó inteiro.
306
- Arquivo dos `outputs` que ainda não existe em disco não conta: uma task `docs` cujos `outputs` ainda
307
- não existem tira **todos** os nós do escopo, e o rollup `tests` sai `skip` (`every test node was
308
- skipped`).
309
- 2. **Quais testes dentro do nó.** Os arquivos de teste que estão nos `outputs` estreitam o comando
310
- declarado — **só** quando ele tem uma forma reconhecida. Qualquer outra coisa roda o nó **inteiro**,
311
- com o motivo em `runs[].selection.reason`.
312
-
313
- | Runner | Forma reconhecida do comando | O que o recorte acrescenta | Prova de que algum teste rodou |
314
- | --- | --- | --- | --- |
315
- | `vitest` | um comando só, com `vitest` e `run` (sem `;`, `&`, `\|` nem `npm`/`pnpm`/`yarn`/`bun run`) | os caminhos dos arquivos, relativos ao `cwd` | contagem > 0 |
316
- | `jest` | um comando só, com `jest` | `--runTestsByPath "<caminho>" …` | contagem > 0 |
317
- | `dotnet`, VSTest | links `dotnet test …` encadeados só por `&&` | `--filter "FullyQualifiedName~<Tipo>\|…"`; link sem projeto, ou com `.sln`, é re-apontado para o `.csproj` dono (`dotnet test "<rel.csproj>" …`) | contagem > 0 **do nó** — numa cadeia, a soma dos links (limite (h)); o VSTest sai 0 com filtro que não casa nada |
318
- | `dotnet`, MTP + xunit.v3 | idem, com `global.json` → `test.runner: Microsoft.Testing.Platform` | `--filter-class "*<Tipo>*"` por tipo; link sem projeto ganha `--project "<rel.csproj>"` | contagem > 0 do sumário MTP (pt-BR e en-US); sem sumário legível, exit 0 (o MTP sai 8 com zero testes) |
319
- | `dotnet`, MTP + MSTest/NUnit | idem | `--filter "FullyQualifiedName~<Tipo>\|…"` | idem |
320
-
321
- `node-test`, `playwright` e `none` não têm recorte dentro do nó: rodam inteiros (nós `e2e: true` e
322
- `evals` seguem pulados no escopo de task, como antes). **Piso de projeto no MTP:** `--minimum-expected-tests
323
- 1` na linha de comando **não** sobrescreve o piso do projeto — um recorte verde sairia 9 —, mas a
324
- propriedade global do MSBuild sim (medido). Por isso, quando o piso vem de **um** lugar só, o recorte
325
- acrescenta `"-p:TestingPlatformCommandLineArguments=<o mesmo valor, com o piso em 1>"`. Um lugar só é: um
326
- único `<TestingPlatformCommandLineArguments>` literal no próprio `.csproj`, sem atributo, dentro de um
327
- `<PropertyGroup>` sem atributo direto sob `<Project>` (nunca num `<Target>`, num `<Choose>`/`<When>` ou num
328
- grupo com `Condition=`), sem `TreatAsLocalProperty` e sem `<Import>` no `.csproj`; sem `$(…)`, `;`, `,`,
329
- aspas, crase, `\`, `!`, `@`, `%` ou entidade no valor; a propriedade ausente do `.csproj.user` e de todo
330
- `Directory.Build.props/.targets/.rsp` e `Directory.Packages.props` até a raiz do **disco** (o MSBuild não
331
- para no repositório); nenhum piso no `*testconfig.json` ao lado; e o comando sem declarar a propriedade ele
332
- mesmo. O `-p:` troca o valor **inteiro**, então os outros argumentos vão junto, literais; verde sai 0, zero
333
- testes segue saindo 8, e nada é recompilado (medido). É o caso do `ai-kit` do framework e dos projetos
334
- copiados dele. Piso em qualquer outra forma, sob VSTest ou num link que roda mais de um projeto roda o nó
335
- **inteiro**. **O que fica fora:** um pacote NuGet cujo `build/*.targets` acrescente argumentos à
336
- propriedade não é lido, e esses argumentos caem durante o recorte; um `--explicit` do xunit.v3 chegando por
337
- aí muda **quais** testes rodam. `<VSTestTestCaseFilter>` declarado também roda o nó inteiro (um `--filter`
338
- acrescentado o substituiria em silêncio).
339
-
340
- **O seletor .NET é o TIPO, sem namespace.** Cada `class`/`record`/`struct`/`interface` declarado nos
341
- arquivos de teste da task, em qualquer profundidade, vira um seletor com o nome **nu**. Medido no MTP +
342
- xunit.v3: `--filter-class "*<Tipo>*"` roda o tipo em qualquer namespace, os aninhados `Outer+Inner`,
343
- arquivo com BOM e namespace global, e todo tipo cujo nome contém o seletor; `FullyQualifiedName~<Tipo>`
344
- é substring, mesma semântica. O recorte pode **acrescentar** testes, nunca tirar os do arquivo. (Sem o
345
- curinga à esquerda, `<Tipo>*` não roda nada — medido, exit 8.)
346
-
347
- **Falha para o lado seguro (.NET).** O `.cs` é lido por uma varredura que não é parser, e o recorte não
348
- depende de ela acertar: toda palavra-chave de tipo contada no **texto cru** do arquivo tem de ser uma
349
- declaração que a varredura nomeou, senão o nó roda inteiro. Também rodam o nó inteiro, cada caso com o
350
- seu motivo:
351
-
352
- - arquivo que declara uma classe `abstract` (os testes rodam sob o nome das derivadas, talvez em
353
- arquivos que a task não tocou);
354
- - arquivo do projeto de teste **sem** atributo de teste — helper, fake, fixture: quem o usa está em
355
- outro arquivo. Atributo de teste é o **nome inteiro** (`Fact`, `Theory`, `Test`, `TestMethod`,
356
- `DataTestMethod`, `TestCase`, `TestCaseSource`, com ou sem `Attribute`, qualificado ou não) numa
357
- seção de atributo: o `, TestContext ctx` ou `, Factory f` do construtor de um helper não conta;
358
- - **qualquer** arquivo que não é C# e que o nó pode rodar (dentro do `cwd`, ou cujo projeto mais
359
- próximo é o de um link) quando o projeto mais próximo dele (`.csproj`/`.fsproj`/`.vbproj`) é de teste —
360
- `.razor` do bUnit, `.feature` do Reqnroll/SpecFlow, snapshot `.verified.txt` do Verify, dado de teste —
361
- ou, fora de projeto de teste reconhecido, `.razor`/`.cshtml`/`.fs`/`.fsi`/`.fsx`/`.vb` que lê como
362
- teste: a varredura só lê C#, então ele não tem seletor, e os testes que ele alimenta podem estar em
363
- arquivos que a task não tocou. Arquivo de projeto que não é de teste (componente ou `appsettings.json`
364
- da app) não custa nada;
365
- - arquivo tocado que parece teste mas o reconhecedor não classifica como tal (nunca é deixado de fora);
366
- - literal ou comentário sem fim, chave desbalanceada, buraco de interpolação cortado por aspas,
367
- diretiva de pré-processador entre a palavra-chave e o nome;
368
- - link cujo projeto não resolve para um `.csproj` existente dentro da raiz (`--project=X.csproj`,
369
- `--project:X.csproj`, `.csproj` fora da raiz);
370
- - projeto que faz o MTP **ignorar um exit code** — `--ignore-exit-code` ou
371
- `TESTINGPLATFORM_EXITCODE_IGNORE` citado em qualquer ponto do `.csproj` ou de um
372
- `Directory.Build.props/.targets` até a raiz (em `<TestingPlatformCommandLineArguments>` ou numa
373
- propriedade que ela referencia, como `$(MtpExtraArgs)`), ou num `*testconfig.json` ao lado — e
374
- `TESTINGPLATFORM_EXITCODE_IGNORE` não vazia no ambiente do verify: sem sumário legível, o exit 0 é a
375
- prova de que o recorte rodou teste, e com o 8 ignorado zero testes saem 0 (roda inteiro mesmo que o
376
- sumário fosse ser legível — isso é decidido antes da rodada);
377
- - comando que já carrega `--filter`/`--treenode-filter`, argumentos depois de `--`,
378
- `--ignore-exit-code` ou `--minimum-expected-tests` (inclusive dentro de `-p:`); `global.json`
379
- ilegível; MTP com framework sem filtro de classe conhecido; alvo MTP de solução ou pasta, alvo VSTest
380
- de pasta; `--solution`, `--directory`, `--test-modules` e `--project` que não aponta um `.csproj` —
381
- em token separado **ou** com o valor depois de `=`/`:` num token só (`--solution=X.sln`,
382
- `--test-modules:…`, `--project=<pasta>`); nome de tipo fora de `[A-Za-z0-9_.]`.
383
-
384
- O dono de um arquivo tocado é o `.csproj` do link, nunca o `cwd` do nó: num `dotnet test --project
385
- ../B.Tests/B.Tests.csproj`, os arquivos de `B.Tests` são desse link. Link cujo projeto não compila
386
- nenhum arquivo da task sai da cadeia. **O custo, dito:** texto com cara de declaração num comentário ou
387
- string (`// the class under test`), restrição genérica seguida de nome (`where T : class where U : …`),
388
- `: class // nota`, `record with`, `$"{d["k"]}"`, atributo de teste por alias
389
- (`using F = Xunit.FactAttribute;`) — tudo isso roda o nó inteiro. Custa relógio, não um teste pulado.
390
-
391
- **O veredito da rodada recortada** é próprio:
392
-
393
- - **Nunca compara com `expectedCount`**: a baseline é da suíte inteira — um recorte que roda 12 de
394
- 3.082 não é queda, e um superconjunto não é alta. Nó rodado **inteiro** no escopo de task continua
395
- com a regra de contagem de sempre.
396
- - Verde exige prova de que rodou teste: contagem > 0 **do nó** (numa cadeia VSTest, a soma dos links —
397
- limite (h)). No MTP a contagem vem do sumário (lido em pt-BR e en-US); sem sumário legível, a prova é
398
- o exit 0 (por isso o que faz o MTP ignorar exit code roda inteiro — acima; o que o varredor não vê é
399
- o limite (f)). Contagem 0 → `narrowed-zero-tests: o recorte executou 0 testes` — no MTP inclusive
400
- com exit 0, que é o que um exit 8 ignorado produz (medido). Como o MTP imprime um bloco de sumário por
401
- invocação, numa cadeia `&&` **cada** bloco é julgado: um bloco com `total: 0` é vermelho
402
- (`narrowed-zero-tests: um link do recorte executou 0 testes …`) mesmo que a soma dos links dê mais que
403
- zero. Verde sem contagem legível (e sem o exit 0
404
- do MTP) → `narrowed-count-unknown` — desconhecido nunca vira verde.
405
- - Vermelho com a assinatura de zero testes do runner (dotnet: exit 8; VSTest: `No test matches the
406
- given testcase filter` / `Nenhum teste corresponde ao filtro de testcase`; vitest: `No test files
407
- found`; jest: `No tests found`) e sem contagem > 0 → `narrowed-zero-tests (<runner>, exit <n>)`.
408
- Qualquer outro vermelho, exit 9 inclusive, mantém a razão crua do executor.
409
- - As contagens de vitest, jest e dotnet são lidas depois de tirar os códigos ANSI da saída.
410
-
411
- **O que fica registrado.** Cada entrada de `runs[]` ganha `selection` — `null` no escopo de feature; no
412
- de task, `mode` (`narrowed` / `whole` / `out-of-scope` / `full`), `reason`, `declaredCommand`, `files`,
413
- `selectors` e `zeroGuard`. `runs[].command` é o que rodou. `nodes.tests.scope` (`scope`, `derivation`,
414
- `group`, `testFiles`) sai só no `--json`, e nada da `selection` vai para o `feature.json`: o stamp grava
415
- `scope: 'task:{id}'`, e o `gate-check` do `review` pausa sobre ele (seção **Gates e estado**). A saída
416
- humana imprime uma linha `↳ {id} {mode} {detalhe}` por nó.
417
-
418
- **Os `outputs` têm de ser os reais.** O recorte lê os `outputs` do `tasks.json` na hora do verify, e
419
- caminho que não existe em disco é ignorado. Por isso o `morph-apply` grava os `outputs` reais **antes**
420
- do verify do loop: com os planejados, um arquivo de teste tocado e ausente deles fica fora do recorte.
421
- Cada caminho é lido na **grafia do disco**: `web/Src/Foo.test.ts` nos `outputs` e `web/src/foo.test.ts`
422
- no disco → o comando recebe `src/foo.test.ts` (o contêiner do `--linux` diferencia maiúsculas, e o jest
423
- pula em silêncio o caminho que não acha). Só a **caixa** vem do disco: caminho que passa por um link
424
- (symlink ou junction) fica como escrito, e o arquivo segue no nó cujo `cwd` o contém.
425
- E uma **pasta** nos `outputs` não estreita nada — quais
426
- arquivos dela a task tocou não se sabe: todo nó cujo `cwd`, ou a pasta do projeto de um link, se
427
- sobrepõe a ela (uma contém a outra) roda **inteiro**, e nunca sai do escopo por ela.
428
-
429
- **`--filter` e `--full`.** `--filter <ids>` força a escolha de nós: desliga a escolha pelo `group`, e o
430
- estreitamento dentro dos nós continua. `--full` desliga as duas decisões — cada nó roda o comando
431
- declarado, com `mode: 'full'` (o `--filter`, se vier junto, continua escolhendo os nós). No escopo de
432
- feature, `--full` é no-op.
433
-
434
- **Limites conhecidos do recorte** — o que ele **não** garante hoje:
435
-
436
- - (a) classe base de teste **não** abstrata cujos testes são herdados por derivadas em arquivos que a
437
- task não tocou: roda a classe tocada; as derivadas, só se o nome delas contiver o da base
438
- (`*ContractTests*` casa `SqlContractTests`, não `SqlTests`);
439
- - (b) `partial class` que é `abstract` só na parte de **outro** arquivo: não é detectada, e o arquivo
440
- tocado estreita;
441
- - (c) link VSTest para `.sln` num nó cujo `cwd` é mais estreito que a solução: é re-apontado só para os
442
- projetos dos arquivos **dentro** do `cwd` — arquivo de teste da task em outro projeto da solução não
443
- roda nesse nó. O mesmo vale para a **escolha** dos nós: a rede da task mal agrupada (e a pasta tocada)
444
- só alcança o nó cujo `cwd`, ou o `.csproj` de um link, contém o arquivo — o `.sln` não é lido, e um nó
445
- que só chega ao arquivo pela solução sai do escopo pelo `group`;
446
- - (d) o atributo de teste é reconhecido pelo nome, sem compilar. A seção tem de **fechar** como seção
447
- de atributo (`]` seguido de declaração ou do fim do arquivo), então coleção em várias linhas com
448
- `Env.Test`, linha `[Env.Test, Env.Dev],` e `Test("a")` não enganam mais. O que ainda engana, sondado:
449
- um **padrão de lista** que abre a linha (ou vem depois de `;` `{` `}` `]`), cujo elemento se chama
450
- exatamente `Test`, `Fact`, … (qualificado ou não) e cujo `]` é seguido de um combinador —
451
- `[Env.Test] or [Env.Dev] => true` num helper faz dele arquivo de teste, que estreita para o nome do
452
- helper. Se um teste real
453
- tiver nome que contém o do helper (`*FakeClock*` casa `FakeClockTests`), esse teste roda e a rodada
454
- pode sair **verde sem rodar os testes que usam o helper**, que estão em arquivos que a task não tocou.
455
- Sem teste com nome assim, o filtro não casa nada: no MTP sai 8 (vermelho); no VSTest é vermelho num nó
456
- de link único, mas numa cadeia de links cai no limite (h). Atributo customizado (`[SkippableFact]`) ou
457
- por alias não é visto: o arquivo conta como helper e o nó roda inteiro (custo, não risco);
458
- - (e) arquivo de teste JS/TS com nome fora de `*.test.*` / `*.spec.*` / `__tests__/` e sem `describe(`
459
- / `it(` / `test(` no início de uma linha não é reconhecido: o nó estreita para os outros arquivos da
460
- task;
461
- - (f) no MTP com o sumário **ilegível** (o `dotnet` num idioma que não é pt-BR nem en-US) a prova volta
462
- a ser só o exit code, e o recorte só vê o que o varredor lê (o comando, o `.csproj`, os
463
- `Directory.Build.props/.targets` até a raiz, o `*testconfig.json` ao lado e o ambiente do verify):
464
- `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` que chegue por um `<Import>` (não é
465
- seguido), por um `Directory.Build.rsp`, por um `.runsettings` ou pela imagem do contêiner do `--linux`
466
- não é visto — o nó estreita, e zero testes podem sair 0: verde. Com o sumário legível o mesmo caso sai
467
- `total: 0` e é vermelho (medido) — numa cadeia `&&` também, porque cada link imprime o próprio bloco;
468
- - (g) dois `.csproj` na mesma pasta: o dono é o primeiro em ordem alfabética;
469
- - (h) numa **cadeia** VSTest (vários links `&&`), a contagem é a **soma** dos links: um link cujo filtro
470
- não casou nada (o VSTest sai 0) fica verde ao lado de um link que contou. "Contagem > 0" é prova por
471
- nó, não por link. No MTP isso não acontece: o link vazio sai 8 e derruba a cadeia — e, se esse 8 for
472
- ignorado, o bloco de sumário dele diz `total: 0`, que derruba o recorte (com o sumário legível).
473
-
474
- A saída de todos é a mesma: `--full`.
714
+ ## Standards
715
+
716
+ ### standards
717
+
718
+ **`morph-spec standards [--find "<consulta>"] [--show <id>[#<âncora>]] [--category <c>] [--tags <a,b>] [--json]`** — escolhe e lê standard sem abrir o registro inteiro nem o `.md` inteiro. Lê `.morph/framework/standards/STANDARDS.json` e, sem ele, o do pacote; os `aliases` vêm do sidecar `STANDARDS.meta.json`, juntados por id.
719
+
720
+ - **Sem flag** — categorias e contagens.
721
+ - **`--find "<consulta>"`** — pontua no servidor (id/nome > tags/aliases/categoria > digest) e devolve o **top 3** com `id`, `path`, `category`, `digest` e as **chaves de âncora** do standard (`âncoras: regra, exemplo… → morph-spec standards --show <id>#<âncora>`). `--category`/`--tags` restringem. Nenhum resultado sai 0 com as categorias disponíveis (é consulta, não gate); `--json` mantém `{ query, results: [], suggestion }`, e cada resultado traz `anchors` como lista de chaves.
722
+ - **`--show <id>`** — o digest e a tabela âncora → slug do heading. `--json`: `{ id, path, digest, anchors }`.
723
+ - **`--show <id>#<âncora>`** — imprime **só aquela seção** do `.md`: do heading até o próximo heading de nível igual ou maior. A âncora é uma chave de `anchors` (`regra`) ou qualquer slug de heading do arquivo (`regra-principal`, com ou sem `#`); a resolução de slug é a mesma que o gerador do registro usa para validar as âncoras (`src/lib/standards/sections.js`). `--json`: `{ id, anchor, slug, section }`.
724
+
725
+ Falhas do `--show` saem 1: id inexistente (`Standard "<id>" não existe.` com até 3 sugestões; `--json` `error: "unknown-id", suggestions`), âncora que não é chave nem heading (`"<âncora>" não é âncora nem heading de <id>.` listando as âncoras; `error: "unknown-anchor"`) e `.md` ilegível (`error: "unreadable"`). `STANDARDS.json` ausente ou inválido também sai 1.
726
+
727
+ **Ordem de leitura:** o digest decide se o standard serve; `--show <id>#<âncora>` traz a seção que a tarefa precisa; o arquivo inteiro só quando a tarefa toca o standard todo.
475
728
 
476
729
  ## Instalação e atualização
477
730
 
478
- - **`morph-spec update [--templates] [--standards] [--dry-run] [--force]`** — reinstala os payloads do framework no projeto (standards, hooks, schemas, evals, `.claude/{commands,rules,skills,agents}`, `.claude/CLAUDE.md`, `.gitignore`/`.gitattributes`, statusline global).
731
+ ### update
479
732
 
480
- **`--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. 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`.
733
+ **`morph-spec update [--templates] [--standards] [--dry-run] [--force]`** — reinstala os payloads do framework no projeto (standards, hooks, schemas, evals, `.claude/{commands,rules,skills,agents}`, `.claude/CLAUDE.md`, `.gitignore`/`.gitattributes`, statusline global).
481
734
 
482
- 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"**:
483
- - **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);
484
- - **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
485
- - **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.
735
+ **`--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.
486
736
 
487
- O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bloquear o relatório justamente quando o usuário está decidindo se atualiza seria inútil — o aviso vira a primeira linha do relatório. `init` delega a `updateCommand` e **nunca** passa `dryRun`.
737
+ 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/evals,config}`, com a contagem de arquivos que hoje moram em cada um;
739
+ - **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
740
+ - **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.
488
741
 
489
- **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.
742
+ O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bloquear o relatório justamente quando o usuário está decidindo se atualiza seria inútil — o aviso vira a primeira linha do relatório. `init` delega a `updateCommand` e **nunca** passa `dryRun`.
743
+
744
+ **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
+
746
+ **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`.
490
747
 
491
748
  ## Grafo de codebase
492
749
 
@@ -496,15 +753,35 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
496
753
 
497
754
  **Por que não consumir o graphify cru.** O resolvedor C# dele não implementa escopo hierárquico de namespace, então um tipo usado por um namespace filho sem `using` explícito vira um *stub* por arquivo — e o blast radius do símbolo real devolve **zero** consumidores onde existem dois. Falso negativo silencioso é o pior modo de falha possível antes de um hotfix. A camada de saneamento coalesce esses stubs (+91,9% de arestas de uso recuperadas no MORPH_GHLBrain), trata como externo só o que **não tem nenhum** nó com arquivo (BCL/NuGet), e devolve as opções quando o símbolo é genuinamente ambíguo em vez de escolher por conta própria.
498
755
 
499
- - **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD`, e também o registro da última rodada do graphify (abaixo) — e importa porque o refresh roda **em background**: enquanto o `graphify update` que um commit disparou corre, o grafo ainda é o do commit anterior. Stale, o status diz o porquê (`freshness.reason`: carimbo antigo, `ignore-changed` ou `rules-unknown`) e o que vai acontecer: refresh em andamento (e desde quando), o último falhou naquele HEAD, o graphify saiu do PATH — ou, numa worktree, que o grafo foi semeado da raiz e só anda com `morph-spec graph refresh`. `--json` devolve `{ available, path, nodes, testNodes, links, freshness: {state, reason?}, refresh: {auto, running, since, pid, last}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous, recoveredDispatch} }`; `refresh.auto` é `true` só na árvore principal.
500
- - **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois — e migra o que uma versão anterior gerou (abaixo). Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace. Com um refresh em background segurando o lock do repositório, sai 0 com `running: true` e não dispara um segundo graphify sobre o mesmo `graph.json`.
501
- - **`morph-spec graph refresh [-p <path>] [--json]`** — `graphify update`, o mesmo comando que o refresh em background roda, com o mesmo registro e, na raiz, o mesmo lock (numa worktree não há lock: ninguém mais escreve o grafo dela). É o único que atualiza o grafo de uma **worktree**. O eval mediu 2 a 3× sobre o build; no MORPH_GHLBrain (1.573 arquivos indexados) ele custou ~80 s mesmo sem mudança nenhuma, o mesmo que o build — o ganho está em não precisar rodar à mão.
502
- - **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores de teste (projeto de teste, `__tests__/`, `*.test.*`, `*.spec.*`); sem ele, cada consumidor traz `test: true|false`. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
503
- - **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`, `recovered-dispatch`), então dá para ver exatamente o que o saneamento recuperou, e `test` diz se a outra ponta é código de teste. É também como se acha o dublê a reusar num teste novo: `graph explain FakeX`.
756
+ ### graph status
757
+
758
+ **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD`, e também o registro da última rodada do graphify (abaixo) — e importa porque o refresh roda **em background**: enquanto o `graphify update` que um commit disparou corre, o grafo ainda é o do commit anterior. Stale, o status diz o porquê (`freshness.reason`: carimbo antigo, `ignore-changed` ou `rules-unknown`) e o que vai acontecer: refresh em andamento (e desde quando), o último falhou naquele HEAD, o graphify saiu do PATH — ou, numa worktree, que o grafo foi semeado da raiz e só anda com `morph-spec graph refresh`. `--json` devolve `{ available, path, nodes, testNodes, links, freshness: {state, reason?}, refresh: {auto, running, since, pid, last}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous, recoveredDispatch} }`; `refresh.auto` é `true` só na árvore principal.
759
+
760
+ ### graph build
761
+
762
+ **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois — e migra o que uma versão anterior gerou (abaixo). Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace. Com um refresh em background segurando o lock do repositório, sai 0 com `running: true` e não dispara um segundo graphify sobre o mesmo `graph.json`.
763
+
764
+ ### graph refresh
765
+
766
+ **`morph-spec graph refresh [-p <path>] [--json]`** — `graphify update`, o mesmo comando que o refresh em background roda, com o mesmo registro e, na raiz, o mesmo lock (numa worktree não há lock: ninguém mais escreve o grafo dela). É o único que atualiza o grafo de uma **worktree**. O eval mediu 2 a 3× sobre o build; no MORPH_GHLBrain (1.573 arquivos indexados) ele custou ~80 s mesmo sem mudança nenhuma, o mesmo que o build — o ganho está em não precisar rodar à mão.
767
+
768
+ ### graph affected
504
769
 
505
- **Refresh automático — só da raiz.** Depois de um comando de shell do agente (Bash ou PowerShell) que chama `git`, `gh` ou `morph-spec` (elo `graph-refresh-kick` do PostToolUse) e no início de cada sessão (SessionStart), o morph-spec solta um processo **destacado** (`.morph/framework/hooks/shared/graph-refresh.js`) que roda `graphify update` na **árvore principal** quando o grafo dela está velho. Quem dispara nunca espera. O processo nasce com `cwd` no git common dir e nunca recebe o caminho de uma worktree: no win32 um diretório em uso não se apaga, e um refresh de worktree correndo fazia o `finish`/`worktree remove` falhar com "Permission denied". "Velho" é a regra única de `shared/graph-freshness.js`: o carimbo `built_at_commit` contra o HEAD e o registro da última rodada em `<git-common-dir>/morph-spec/graph-refresh-state.json`. O registro existe porque o `graphify update` do 0.9.50 **não recarimba** quando a topologia não muda ("outputs left untouched" — commit só de `.morph/`, corpo de método editado): uma rodada com exit 0 no HEAD H vale como grafo em dia em H, uma que falhou em H só se repete quando o HEAD andar, e graphify ausente fica registrado (o disparo não solta processo até ele voltar ao PATH). As regras do `.graphifyignore` entram por **conteúdo** — o registro guarda o hash das regras com que o grafo foi extraído; comentário, cabeçalho, BOM e CRLF não contam. Um lock por repositório (`graph-refresh.lock`, renovado a cada rodada) impede dois graphify no mesmo `graph.json`, a checagem é refeita depois de soltá-lo, e cada tentativa vai para `graph-refresh.log`. `MORPH_GRAPH_AUTO_REFRESH=0` desliga. **Limite:** um commit feito fora do agente (terminal, IDE) só é alcançado no próximo início de sessão ou no próximo comando git do agente. O hook git do próprio graphify (`graphify hook install`) **não** é usado: medido no 0.9.50, ele sai sem fazer nada em worktree vinculada, não tem `post-merge` (merge, fast-forward e `pull` nunca rebuildam) e, num commit depois de um fast-forward, carimba o HEAD num grafo sem os arquivos do merge. O `doctor` avisa quando ele está instalado.
770
+ **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores de teste (projeto de teste, `__tests__/`, `*.test.*`, `*.spec.*`); sem ele, cada consumidor traz `test: true|false`. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
506
771
 
507
- **Grafo em worktree.** `worktree setup`/`provision` copiam o grafo da raiz para a worktree nova (só `graphify-out/`, sem `.graphify_root`, que guarda o caminho absoluto da raiz, e sem `cache/`), no exclude do common dir, com o registro da raiz: o commit em que ele está em dia e as regras com que foi extraído. O `.graphifyignore` **não** vem: a worktree lê o do próprio checkout, e um `/.graphifyignore` no exclude do common dir escondia também o da raiz ainda não versionado. Uma worktree anterior a isso ganha a cópia no próximo SessionStart. Como a worktree nasce de um commit da raiz, o grafo copiado já é o do HEAD dela ou fica a um `morph-spec graph refresh` de distância — e o validador `reuse` do `verify`, que lê o grafo da árvore em que roda, deixa de pular justamente onde a feature é feita. **Dali em diante a worktree só anda com `morph-spec graph refresh`**, em primeiro plano: o refresh automático não a toca (acima). Ali o `graph build`/`refresh` não espera o lock da raiz e nunca reescreve o `.graphifyignore` do checkout; sem um (a raiz ainda não o versionou), usa as regras da raiz só durante a rodada e as tira depois — um `?? .graphifyignore` deixado na worktree faria o `finish` e o `worktree remove --if-clean` recusarem com `worktree-dirty`. A cópia leva um comentário-marcador na 1ª linha: se o processo morrer antes de tirá-la, a próxima rodada a reusa, e o `finish` e o `worktree remove` não a contam como sujeira e só a apagam depois de todas as recusas. O graphify roda com `cwd` no git common dir, nunca na árvore: ele lê o alvo pelo argumento, e um graphify órfão com `cwd` numa worktree a prendia no win32 até terminar. As regras que valem para o frescor da worktree são as mesmas: sem arquivo próprio, as da raiz — uma mudança nelas (o `update` migrando o ignore) deixa a worktree `stale` com `ignore-changed`. O `finish` trata `graphify-out/` como artefato descartável.
772
+ ### graph explain
773
+
774
+ **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`, `recovered-dispatch`), então dá para ver exatamente o que o saneamento recuperou, e `test` diz se a outra ponta é código de teste. É também como se acha o dublê a reusar num teste novo: `graph explain FakeX`.
775
+
776
+ ### Refresh automático, worktree e testes no grafo
777
+
778
+ **Refresh automático — só da raiz.** Depois de um comando de shell do agente (Bash ou PowerShell) que chama `git`, `gh` ou `morph-spec` (elo `graph-refresh-kick` do PostToolUse) e no início de cada sessão (SessionStart), o morph-spec solta um processo **destacado** (`.morph/framework/hooks/shared/graph-refresh.js`) que roda `graphify update` na **árvore principal** quando o grafo dela está velho. Quem dispara nunca espera.
779
+
780
+ "Velho" é a regra única de `shared/graph-freshness.js`: o carimbo `built_at_commit` contra o HEAD e o registro da última rodada em `<git-common-dir>/morph-spec/graph-refresh-state.json`. O registro existe porque o `graphify update` do 0.9.50 **não recarimba** quando a topologia não muda ("outputs left untouched" — commit só de `.morph/`, corpo de método editado): uma rodada com exit 0 no HEAD H vale como grafo em dia em H, uma que falhou em H só se repete quando o HEAD andar, e graphify ausente fica registrado (o disparo não solta processo até ele voltar ao PATH). As regras do `.graphifyignore` entram por **conteúdo** — o registro guarda o hash das regras com que o grafo foi extraído; comentário, cabeçalho, BOM e CRLF não contam. Um lock por repositório (`graph-refresh.lock`, renovado a cada rodada) impede dois graphify no mesmo `graph.json`, a checagem é refeita depois de soltá-lo, e cada tentativa vai para `graph-refresh.log`. `MORPH_GRAPH_AUTO_REFRESH=0` desliga.
781
+
782
+ **Limite:** um commit feito fora do agente (terminal, IDE) só é alcançado no próximo início de sessão ou no próximo comando git do agente. O `doctor` avisa quando ele está instalado.
783
+
784
+ **Grafo em worktree.** `worktree setup`/`provision` copiam o grafo da raiz para a worktree nova (só `graphify-out/`, sem `.graphify_root`, que guarda o caminho absoluto da raiz, e sem `cache/`), no exclude do common dir, com o registro da raiz: o commit em que ele está em dia e as regras com que foi extraído. O `.graphifyignore` **não** vem: a worktree lê o do próprio checkout, e um `/.graphifyignore` no exclude do common dir escondia também o da raiz ainda não versionado. Uma worktree anterior a isso ganha a cópia no próximo SessionStart. Como a worktree nasce de um commit da raiz, o grafo copiado já é o do HEAD dela ou fica a um `morph-spec graph refresh` de distância — e o validador `reuse` do `verify`, que lê o grafo da árvore em que roda, deixa de pular justamente onde a feature é feita. **Dali em diante a worktree só anda com `morph-spec graph refresh`**, em primeiro plano: o refresh automático não a toca (acima). Ali o `graph build`/`refresh` não espera o lock da raiz e nunca reescreve o `.graphifyignore` do checkout; sem um (a raiz ainda não o versionou), usa as regras da raiz só durante a rodada e as tira depois — um `?? .graphifyignore` deixado na worktree faria o `finish` e o `worktree remove --if-clean` recusarem com `worktree-dirty`. A cópia leva um comentário-marcador na 1ª linha: se o processo morrer antes de tirá-la, a próxima rodada a reusa, e o `finish` e o `worktree remove` não a contam como sujeira e só a apagam depois de todas as recusas. As regras que valem para o frescor da worktree são as mesmas: sem arquivo próprio, as da raiz — uma mudança nelas (o `update` migrando o ignore) deixa a worktree `stale` com `ignore-changed`. O `finish` trata `graphify-out/` como artefato descartável.
508
785
 
509
786
  **Testes no grafo.** Em .NET o conteúdo dos projetos de teste fica fora, mas dublês, fixtures e helpers (`TestDoubles/`, `Fakes/`, `Stubs/`, `Mocks/`, `Fixtures/`, `Helpers/`, `Builders/`, `*Fixture.cs`, `*Fixtures.cs`, `Fake*.cs`, `Stub*.cs`, `*Stub.cs`) entram, marcados como teste. A forma do `.graphifyignore` segue a regra do gitignore que o graphify aplica — um `!` não reinclui nada sob diretório excluído —, então o arquivo exclui o conteúdo (`**/*.Tests/**`), reinclui os diretórios e só então os helpers, com `bin/`/`obj/` por último. Nó de teste fica fora dos god nodes e não conta grau; os `dispatches_to` que o graphify perde quando um fake vira o segundo implementador de uma interface são recuperados pelo saneamento (no MORPH_GHLBrain: de 122 para 5 no graphify, 122 de volta). Um `.graphifyignore` idêntico ao que a versão anterior gerava é trocado pelo atual no `update`, no `setup-infra` e no `graph build`/`refresh` — só quando as **regras** mudam (sem .NET a diferença era só o cabeçalho, e o arquivo fica como está); versionado, o `update` avisa para commitá-lo, porque cada checkout lê o seu. Um editado não é tocado, e o aviso diz as linhas a trocar.
510
787
 
@@ -512,26 +789,37 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
512
789
 
513
790
  O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está registrado em `framework/mcp/registry.json` para exploração interativa e consulta cross-repo — mas ele devolve o grafo **cru**, sem saneamento. Para qualquer decisão, prefira os comandos acima.
514
791
 
792
+ **Por quê.** O processo nasce com `cwd` no git common dir e nunca recebe o caminho de uma worktree: no win32 um diretório em uso não se apaga, e um refresh de worktree correndo fazia o `finish`/`worktree remove` falhar com "Permission denied". O hook git do próprio graphify (`graphify hook install`) **não** é usado: medido no 0.9.50, ele sai sem fazer nada em worktree vinculada, não tem `post-merge` (merge, fast-forward e `pull` nunca rebuildam) e, num commit depois de um fast-forward, carimba o HEAD num grafo sem os arquivos do merge. O graphify roda com `cwd` no git common dir, nunca na árvore: ele lê o alvo pelo argumento, e um graphify órfão com `cwd` numa worktree a prendia no win32 até terminar.
793
+
515
794
  ## Observabilidade
516
795
 
517
- - **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log`. Lê também os arquivos rotacionados em `.morph/logs/sessions/`, sem contar duas vezes a mesma linha.
518
- - **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), 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`.
519
- - **`morph-spec cost [feature] --metrics [--by task] [--all-trees] [--top <n>] [--json]`** — as medições do loop, sempre pelo mesmo método, para comparar uma janela com outra:
520
-
521
- | Medida | Definição |
522
- |---|---|
523
- | turnos | eventos `ToolBatch` (um por turno do modelo que usou ferramenta), orquestrador × sub-agent; `null` se a janela não tem nenhum |
524
- | chamadas de ferramenta | `PostToolUse` + `PostToolUseFailure`, orquestrador (`agentId` ausente num evento v2) × sub-agent × sem agente (v1) |
525
- | tempo por classe | soma de `durationMs` do Bash por classe (`test`, `build`, `verify`, `e2e`, `git`, `morph-spec`, `read`, `other`), com a contagem de chamadas sem duração |
526
- | rerun | o mesmo comando **produtor** (antes do primeiro `\|` fora de aspas, sem `2>&1`) na mesma feature até 10 min depois do anterior; `rate` conta as repetições, `burstRate` toda execução que cai numa rajada, a primeira incluída. `rerunClasses`: `fix-loop` (falhou, houve edição), `flake` (falhou, sem edição, passou), `retry` (falhou de novo sem edição), `re-verify` (passou, houve edição), `redundant` (passou, sem edição), `timeout`, `refilter` (mesmo produtor, outro filtro de saída, sem edição: rodou de novo para ver outras linhas), `unknown-after-edit`/`unknown-no-edit` (exit mascarado por pipe ou evento v1) |
527
- | releitura | bytes da mesma janela (arquivo + `offset`/`limit`) lida de novo na mesma feature, com os arquivos de maior releitura; `windowed` conta as leituras com `bytesRead`. As outras (v1) contam o arquivo inteiro, e a taxa vira um teto |
528
- | verify de feature | `morph-spec verify <f>` executado (fora de aspas e de heredoc) por feature, com a mediana; `verify.unchangedRepeats` conta, por feature, o evento `Verify` de escopo de feature cujo `fingerprint` é igual ao do anterior |
529
- | suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
530
- | git por task | ocorrências de `git status` e `git diff` por task, com a mediana |
531
- | por task, pela feature | `perTaskByFeature`: total da feature (chamadas, turnos, `git status`+`diff`) ÷ tasks dela, mediana entre as features. Não depende das janelas `TaskStart`→`TaskEnd`, que um batch paralelo ou um `in_progress` tardio deixam curtas; `taskWindowCoverage` diz que fração das chamadas cai dentro de alguma janela |
532
- | reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
533
-
534
- Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit code ou duração, eles caem em "sem agente"/`unknown-*`/"sem duração", e o cabeçalho diz que fração da janela está no esquema v2.
796
+ ### telemetry
797
+
798
+ **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log`. Lê também os arquivos rotacionados em `.morph/logs/sessions/`, sem contar duas vezes a mesma linha.
799
+
800
+ ### cost
801
+
802
+ **`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`.
803
+
804
+ ### cost --metrics
805
+
806
+ **`morph-spec cost [feature] --metrics [--all-trees] [--top <n>] [--json]`** — as medições do loop, sempre pelo mesmo método, para comparar uma janela com outra:
807
+
808
+ | Medida | Definição |
809
+ |---|---|
810
+ | turnos | eventos `ToolBatch` (um por turno do modelo que usou ferramenta), orquestrador × sub-agent; `null` se a janela não tem nenhum |
811
+ | chamadas de ferramenta | `PostToolUse` + `PostToolUseFailure`, orquestrador (`agentId` ausente num evento v2) × sub-agent × sem agente (v1) |
812
+ | tempo por classe | soma de `durationMs` do Bash por classe (`test`, `build`, `verify`, `e2e`, `git`, `morph-spec`, `read`, `other`), com a contagem de chamadas sem duração |
813
+ | rerun | o mesmo comando **produtor** (antes do primeiro `\|` fora de aspas, sem `2>&1`) na mesma feature até 10 min depois do anterior; `rate` conta as repetições, `burstRate` toda execução que cai numa rajada, a primeira incluída. `rerunClasses`: `fix-loop` (falhou, houve edição), `flake` (falhou, sem edição, passou), `retry` (falhou de novo sem edição), `re-verify` (passou, houve edição), `redundant` (passou, sem edição), `timeout`, `refilter` (mesmo produtor, outro filtro de saída, sem edição: rodou de novo para ver outras linhas), `unknown-after-edit`/`unknown-no-edit` (exit mascarado por pipe ou evento v1) |
814
+ | releitura | bytes da mesma janela (arquivo + `offset`/`limit`) lida de novo na mesma feature, com os arquivos de maior releitura; `windowed` conta as leituras com `bytesRead`. As outras (v1) contam o arquivo inteiro, e a taxa vira um teto |
815
+ | verify de feature | `morph-spec verify <f>` executado (fora de aspas e de heredoc) por feature, com a mediana; `verify.unchangedRepeats` conta, por feature, o evento `Verify` cujo `fingerprint` é igual ao do anterior |
816
+ | suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
817
+ | git | ocorrências de `git status` e `git diff` na janela |
818
+ | reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
819
+
820
+ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit code ou duração, eles caem em "sem agente"/`unknown-*`/"sem duração", e o cabeçalho diz que fração da janela está no esquema v2.
821
+
822
+ ### Esquema do evento (v2)
535
823
 
536
824
  **Esquema do evento (v2).** Todo evento carrega `ts`, `schemaVersion: 2`, `eventId` (chave de dedupe entre log, arquivos rotacionados e backups), `feature`, `phase` e, quando o payload traz, `sessionId`, `promptId`, `agentId`/`agentType` (só dentro de sub-agent), `cwd`. Por tipo:
537
825
 
@@ -540,6 +828,6 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
540
828
  | `PostToolUse` / `PostToolUseFailure` | `tool`, `toolUseId`, `durationMs`; Bash/PowerShell: `command` (redigido, ≤2000 chars) + `commandChars`, `exitCode`, `outcome` (`ok`/`error`/`timeout`/`interrupted`/`background`), `exitMasked` quando o exit gravado não é o do trabalho (pipe sem `pipefail`, `|| …`, `&` em background, ou `;` seguido de `echo`/`true`/`:` no fim); Read: `target`, `kind`, `bytes` (tamanho do arquivo), `offset`, `limit`, `bytesRead` — um Read que falhou não grava bytes, porque não trouxe nada; Agent: `persona`, `desc`, `prompt` + `promptChars`; SendMessage: `target`, `messageChars` |
541
829
  | `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
542
830
  | `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
543
- | `Verify` | `scope`, `task`, `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
831
+ | `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
544
832
 
545
833
  Nada da saída de um comando é gravado — nem o corpo de erro do `PostToolUseFailure`, onde um valor de `.env` pode aparecer sem nome ao lado. `command`, `prompt`, `desc` e `target` passam por redação de segredos óbvios antes de gravar: valor ao lado de um nome de segredo (`API_KEY=…`, `*_KEY`/`*_PASS=…`, `--password …`, `user-secrets set <k> <v>`, `AccountKey=…`), `Authorization: …`, `user:pass@` (senha com `/` inclusive) e `?sig=`/`?access_token=` em URL, e formatos de token (GitHub, `sk-`, `sk_live_`, `npm_`, Slack, AWS, Google, JWT). É por padrão, não por garantia: um valor sem nome ao lado passa.