@polymorphism-tech/morph-spec 9.7.0 → 10.0.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -12,7 +12,7 @@
12
12
 
13
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 fora de teste (`securityReviewFor`; spec, `e2e/` e `test-results/` não contam) — 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
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.
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`, plano que cita `.cs`/`.csproj`/`.sln` ou `dotnet test|build|new` 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
16
 
17
17
  **Exceção `hotfix`:** esse tipo nunca despacha (`morph-hotfix` §7); o Gate 3 exige só `4-review/verifier.md`, escrito pelo orquestrador, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
18
18
 
@@ -28,21 +28,21 @@
28
28
 
29
29
  ### advance
30
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.
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 todo `Cn` do `checks.json` `feito` em `3-implement/report.md` (pendente ou `bloqueado` recusa, nomeando os `Cn`; sem `checks.json`, recusa nomeando-o; feature anterior à 10.0 fecha pelos relatórios de fatia). Abrir o implement semeia o `report.md` com todo `Cn` `pendente`; `--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
32
 
33
33
  ### gate-check
34
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.
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`, `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
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.
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`/`irreversivel`; um `irreversivel` aberto vai direto ao humano); 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
38
 
39
39
  Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
40
40
 
41
41
  **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `not-applicable` ("not applicable: …", a feature sem cenário `prova: e2e` e sem arquivo de UI, spec e2e ou container no diff) são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. `e2eStatus`/`e2eReason`/`e2eSkipClass` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`.
42
42
 
43
- **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").
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 — o `plan.md` 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`, em `src/lib/plan/media.js`, é onde essa normalização mora e é testada; o `plan-check` recusa o plano que fala em mídia gerada sem a citação). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (`framework/evals/review-checklist.md`, "Feature que gera mídia").
44
44
 
45
- **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". 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`.
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, e para cada estado que o checklist diz que se aplica (`vazio` e `carregando` pelo item "Estados", `erro` pelo item "Erro"; "não se aplica" dispensa) o `data-estado="<estado>"` no HTML (o seletor de estado) e o print `<tela>-<estado>-390.png`; faltando um, **pausa** nomeando a tela e o arquivo ("protótipo incompleto"), pelo mesmo predicado. No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `verifier.md`, e a linha que o verificador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
46
46
 
47
47
  **Por quê.** Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
48
48
 
@@ -61,20 +61,25 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
61
61
  | `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (`project` e `not-applicable` passam). Só no **ship** |
62
62
  | `promptModel` | prompt/modelo tocado | Sem nó `evals` declarado, ou eval pulado neste verify. Só no **ship** |
63
63
  | `mediaClaims` | seção ausente | Feature que gera mídia, relatório sem `## Mídia gerada`. Só no **ship** |
64
- | `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo e prints 1280/390. **ship**: item decidido sem linha em `## Checklist de tela` |
64
+ | `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo, prints 1280/390, ou seletor e print de cada estado. **ship**: item decidido sem linha em `## Checklist de tela` |
65
65
  | `planChecks` | ≠ `fresh` | `checks.json` ausente ou de outra versão do `plan.md`: rode `plan-check`. Só no **plano**; `approve`/`advance` automáticos recusam |
66
- | `sliceCount` | > 12 | Plano grande demais para uma feature |
67
66
  | `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
67
+ | `planCritique` | `missing`/`no-hash`/`stale` | `2-plan/critica.md` ausente, sem `**Plan-Hash:**`, ou de outra versão do plano sem `**Aplicado-em:**` que cubra a atual (`signal: plan-critique`). Só no **plano**, `chore`/`hotfix` isentos (`exempt`); `approve`/`advance` automáticos recusam |
68
+ | `rungs` | `Cn` com `degrau: migrar` ou `irreversível` (`checks.json`) | **plano**: pausa nomeando os `Cn` (`signal: rung`), e `approve`/`advance` automáticos recusam. **ship**: pausa se o plano não teve aprovação humana (chore, policy) |
69
+ | `migrationsTouched` | caminho `migrations/` no diff, `rungs` vazio | Feature mexeu em dado sem declarar o degrau (`signal: rung-undeclared`). Só no **ship** |
70
+ | `contractsTouched` | `openapi.{yaml,yml,json}`, `swagger.{…}` ou `.proto` no diff, sem `Cn` `irreversível` | O arquivo de contrato mudou sem o degrau (`signal: rung-undeclared`). Webhook e endpoint ficam com o bloqueador `irreversivel` do revisor. Só no **ship** |
71
+
72
+ **Quem resolve a pausa (`resolver`).** Toda pausa leva `resolver` no `--json`, no `feature.json` e no evento `Gate` (`pauseResolver`, `src/lib/risk-detector.js`). `session`: a sessão faz o que a razão diz e roda o `gate-check` de novo, sem perguntar. Valem para os sinais `verify-red`, `verify-missing`, `verify-stale`, `e2e-skip` (só a classe `requested`), `plan-checks`, `plan-critique`, `uiux-checklist`, `review-reports`, `screen-checklist` e `media-claims`. `human`: todo outro sinal (entre eles `rung` e `rung-undeclared`, avaliados por último), a pausa manual do `gate-decision` e os dois tetos — `verify-red` com `failStreak` ≥ `FIX_ATTEMPTS` (o `escalate` do `next`) e sinal de relatório com `FIX_ATTEMPTS` rodadas de review ou mais (bloqueador disputado).
68
73
 
69
74
  | Gate | Sinais | Skill responsável |
70
75
  |---|---|---|
71
- | Plano | `designSystemDivergence`, `standardsViolations`, `planChecks`, `sliceCount` | `morph-plan` |
76
+ | Plano | `designSystemDivergence`, `standardsViolations`, `planChecks`, `planCritique`, `rungs` | `morph-plan` |
72
77
  | uiux — Design | `screenChecklist` | `morph-uiux` |
73
- | 3 — Review | `reviewReports`, `standardsViolations` em implementation, `verificationStale`, `e2eSkipClass`, `promptModel`, `mediaClaims`, `screenChecklist` | `morph-review` |
78
+ | 3 — Review | `reviewReports`, `standardsViolations` em implementation, `verificationStale`, `e2eSkipClass`, `promptModel`, `mediaClaims`, `screenChecklist`, `rungs`, `migrationsTouched`, `contractsTouched` | `morph-review` |
74
79
 
75
80
  ### gate-decision
76
81
 
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.
82
+ **`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"` quando a entrevista foi encerrada com nós abertos, 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
83
 
79
84
  ### Guarda de árvore dona
80
85
 
@@ -142,7 +147,21 @@ Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed:
142
147
 
143
148
  ### archive
144
149
 
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.
150
+ **`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. O arquivo guarda só `feature.json`, `2-plan/plan.md` e o recap (`recap.html` + `recap.json`, gerados antes da poda, enquanto os relatórios do Gate 3 ainda existem): o plano sobrevive até a um squash merge, e `feature.json.archivedFrom.sha` é o topo da branch antes da poda (`git show <sha>:.morph/features/<f>/3-implement/report.md`; num squash, o `(#N)` do commit leva ao PR e a `git fetch origin pull/N/head`).
151
+
152
+ ### recap
153
+
154
+ **`morph-spec recap <feature> [--piper] [--json]`**: o recap animado da feature, uma página com cinco cenas.
155
+ - **As cenas:** abertura com a duração, cenários e o que o verificador provou, a máquina de estados do `## Estados`, o que mudou e por quais gates passou.
156
+ - **"O que mudou":** sai do `graph diff` quando o grafo está em dia e as linhas batem; senão, do `git diff --shortstat` da branch.
157
+ - **Legenda e narração:** cada cena tem legenda. A narração é a voz do sistema pela Web Speech do navegador, sem instalar nada; o botão "Narração" desliga.
158
+ - **Onde fica:** o `finish` (via `archive`) já grava a página no archive.
159
+ - **O comando:**
160
+ - numa feature ativa, regenera a partir da pasta e do diff da branch;
161
+ - numa arquivada, regenera a partir do `recap.json` congelado.
162
+ - **`--piper`:** narra com o Piper local quando há o binário `piper` no PATH (`pip install piper-tts`) e um modelo de voz (`pt_BR-faber-medium.onnx` em `~/.morph-spec/piper/`, ou `MORPH_PIPER_MODEL`).
163
+ - O áudio vai embutido numa cópia gravada no diretório temporário, nunca no archive versionado.
164
+ - Sem Piper, ou se uma cena falhar, fica a voz do sistema, e o `note` diz por quê.
146
165
 
147
166
  ### worktree setup, provision, link, list
148
167
 
@@ -152,7 +171,7 @@ O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a
152
171
 
153
172
  **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
173
 
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.
174
+ **Base da árvore de task.** O hook cria a árvore de task sempre a partir da branch default. `provision --base morph/{f}` (à mão) grava `MORPH_FEATURE={f}` no `worktree.env` — 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
175
 
157
176
  **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
177
 
@@ -164,7 +183,7 @@ O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a
164
183
 
165
184
  **`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
185
 
167
- Derruba o stack compose da árvore e os `e2e.hostServices`: de branch `morph/{f}` **sem** apagar volumes (a feature segue aberta, `down --remove-orphans`); de branch `morph-task/*` **com** volumes (`down --remove-orphans -v`: a task é descartável, e o `worktree reap` herda por passar por aqui). 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).
186
+ Derruba o stack compose da árvore e os `e2e.hostServices`: de branch `morph/{f}` **sem** apagar volumes (a feature segue aberta, `down --remove-orphans`); de branch `morph-task/*` **com** volumes (`down --remove-orphans -v`: a task é descartável). 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
187
 
169
188
  **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
189
 
@@ -172,22 +191,6 @@ Derruba o stack compose da árvore e os `e2e.hostServices`: de branch `morph/{f}
172
191
 
173
192
  **Por quê.** A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho.
174
193
 
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
194
  ### .vscode/settings.json
192
195
 
193
196
  **`.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.
@@ -212,7 +215,7 @@ Desliga: `worktree.editorSettings: false` na config, ou `MORPH_EDITOR_SETTINGS=0
212
215
 
213
216
  **"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
217
 
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.
218
+ 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 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
219
 
217
220
  **Por quê.** Um root **acima** da raiz (`..`) é aceito: o contêiner vira um irmão do repositório.
218
221
 
@@ -220,13 +223,13 @@ O resolvedor é um só (`framework/hooks/shared/worktree-root.js`; hook não imp
220
223
 
221
224
  **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
225
 
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*`.
226
+ **O que a criação nativa daria e o morph faz por conta própria, porque o hook a substitui:** `.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) num repo .NET+Next.js em produção, 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 nesse repo em vez dos 4 que casam) e a cópia de `.env*`.
224
227
 
225
228
  **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
229
 
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.
230
+ **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. 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 resíduo aparece no `doctor --worktrees` como `unintegrated-work` (com trabalho) ou sai por `worktree remove --target`.
228
231
 
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.
232
+ **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` e `worktree remove --target` rodam da raiz, não de dentro.
230
233
 
231
234
  **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
235
 
@@ -258,17 +261,13 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
258
261
 
259
262
  **`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
263
 
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
264
  ### next
266
265
 
267
- **`morph-spec next <feature> [--json]`** — o próximo passo do loop, decidido do disco na árvore dona (mesma resolução do `handoff`). Somente leitura. Ações: `plan` (sem `checks.json`), `gate` (plano extraído), `design` (uiux), `dispatch` (fatias prontas, com as `PARCIAL` nomeadas para retomar), `decide` (nenhuma pronta e alguma com relatório `BLOQUEADO`: decida pelo plano/ADR ou `AskUserQuestion` e retome o mesmo agente, nunca re-despache), `blocked` (pendentes sem dependência satisfeita), `verify` (todas relatadas e sem stamp, ou a árvore andou desde ele — `treeMovedSince`, a régua do `gate-check`), `fix`, `escalate`, `review`, `finish`. **Escalada:** `verify` persistido grava `verifications.feature.failStreak` (vermelhos seguidos; verde zera; rodada parcial ou `--no-persist` não conta); com o stamp vermelho e a árvore parada, `failStreak` múltiplo de 3 é `escalate` → `AskUserQuestion` (aceitar/continuar/parar); o `reason` diz o que cada resposta faz (aceitar → review, onde o verify vermelho pausa o gate e o humano assina). A resposta não é gravada: o `escalate` **persiste até a árvore andar** — releia a resposta da conversa em vez de perguntar de novo. "Continuar" segue de onde parou: a próxima escalada vem 3 vermelhos depois. **Por tipo:** `hotfix` troca toda skill pela `/morph-hotfix` (o pipeline inteiro mora lá); `chore`/`hotfix` nascem em implement sem `checks.json` → `plan` (plano curto e `plan-check`). A regra é UMA (`decideNext`, `framework/hooks/shared/next-step.js`) e tem três leitores: este comando, o `status` e o `handoff`, e o SessionStart, que injeta a linha `▶ próximo:` sem git (verde lido como fresco) — e por isso nunca injeta `escalate` (pede `morph-spec next`) nem o próximo passo do esqueleto da raiz de uma feature que vive num worktree (aponta o worktree). `--json`: `{ action, command, reason, slices?, phase, failStreak, feature, tree }`.
266
+ **`morph-spec next <feature> [--json]`** — o próximo passo do loop, decidido do disco na árvore dona. Somente leitura. Ações: `plan` (sem `checks.json`), `gate` (plano extraído), `design` (uiux), `implement` (os `Cn` ainda não `feito` no `3-implement/report.md`, nomeados), `decide` (algum `Cn` `bloqueado`: decida pelo plano/ADR ou `AskUserQuestion`), `verify` (todo `Cn` `feito` e sem stamp, ou a árvore andou desde ele — `treeMovedSince`, a régua do `gate-check`), `fix`, `escalate`, `review`, `finish`. **Escalada:** `verify` persistido grava `verifications.feature.failStreak` (vermelhos seguidos; verde zera; rodada parcial ou `--no-persist` não conta); com o stamp vermelho e a árvore parada, `failStreak` múltiplo de 3 é `escalate` → `AskUserQuestion` (aceitar/continuar/parar); o `reason` diz o que cada resposta faz (aceitar → review, onde o verify vermelho pausa o gate e o humano assina). A resposta não é gravada: o `escalate` **persiste até a árvore andar** — releia a resposta da conversa em vez de perguntar de novo. "Continuar" segue de onde parou: a próxima escalada vem 3 vermelhos depois. **Por tipo:** `hotfix` segue a `morph-apply` por `references/hotfix.md`; `chore`/`hotfix` nascem em implement sem `checks.json` → `plan` (plano curto e `plan-check`). A regra é UMA (`decideNext`, `framework/hooks/shared/next-step.js`) e tem três leitores: este comando, o `status` e o SessionStart, que injeta a linha `▶ próximo:` sem git (verde lido como fresco) — e por isso nunca injeta `escalate` (pede `morph-spec next`) nem o próximo passo do esqueleto da raiz de uma feature que vive num worktree (aponta o worktree). **Como:** cada ação leva `how`, 1–3 linhas imperativas com os comandos e arquivos exatos do passo (o `verify` sem filtro e `run_in_background` acima de 10 min, a evidência e o commit `wip(<f>)` de cada `Cn`, a pergunta de gate que nomeia a feature, o header "Integração" do `finish`; hotfix abre com `references/hotfix.md`), mantidas só em `next-step.js`; a saída humana as imprime abaixo da linha `próximo:` e o SessionStart não as injeta. `--json`: `{ action, command, reason, how, checks?, phase, failStreak, feature, tree }`.
268
267
 
269
268
  ### fleet
270
269
 
271
- **`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.
270
+ **`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 e atividade. Use entre sessões para decidir onde atuar.
272
271
 
273
272
  ### claim
274
273
 
@@ -278,9 +277,9 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
278
277
 
279
278
  ### verify
280
279
 
281
- **`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.
280
+ **`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. Antes da cadeia, instala em `.claude/rules/` (fora do git) a regra de stack que falta, pela stack do `config.json` somada à que os arquivos mostram (em stderr; nunca muda o veredito). Nunca grava o `config.json`: arquivo versionado mudado pelo próprio verify tornaria a prova velha e a árvore suja para o `--ci`; a tag nova entra pelo `plan-check` ou pelo `update`.
282
281
 
283
- O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável, sobe os `e2e.hostServices` (ver `e2e` abaixo) 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).
282
+ O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável, sobe os `e2e.hostServices` (ver `e2e` abaixo) 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). Sem compose mas com `e2e.hostServices`, a stack é **só de host**: sobem só os serviços do host, sem lint de isolamento do compose nem checagem de Docker, e as specs rodam contra eles. Sem compose e sem `e2e.hostServices`, o nó pula com `project does not containerize (no compose file)`; com a integração `docker` declarada e sem compose, falha.
284
283
 
285
284
  **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 --noEmit` e, com `eslint.config.*` e o pacote `eslint` instalados, `eslint --quiet` só nos arquivos que a feature mudou contra a base), 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`.
286
285
 
@@ -290,13 +289,15 @@ As specs são procuradas na raiz, em `project.frontendPath` e, sem ele, nas past
290
289
 
291
290
  **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.
292
291
 
293
- **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.
292
+ **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) > `timeoutMs` do nó > `project.testTimeoutMs` no `config.json` > **30 min por projeto de teste encadeado** (o build também tem teto de 30 min) — uma suíte longa nunca nasce condenada pelo relógio. 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.
293
+
294
+ **Falha nomeada:** num nó de testes vermelho, o `feedback[]` traz primeiro `[tests/{id}] falhou: <teste>` (até 10, lidas da saída **inteira**: `not ok` do TAP do `node --test` — só as folhas, a suíte-mãe sai —, `FAIL`/`×` do vitest, `failed`/`Failed`/`com falha` do dotnet), depois `[tests/{id}] log completo: .morph/logs/verify-tests-{id}.log` e só então a cauda de 20 linhas; o `reason` do nó resume as três primeiras. Runner que nenhuma forma reconhece fica só com o caminho do log e a cauda. O log é gravado a cada run vermelho (e no `--linux`); erro de escrita não muda o veredito.
294
295
 
295
296
  **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build`, reconstrói e reinicia os `e2e.hostServices` e força o seed de auth a rodar de novo.
296
297
 
297
298
  **`--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.
298
299
 
299
- **`--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.
300
+ **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (e apaga o dos demais) (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.
300
301
 
301
302
  **`--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.
302
303
 
@@ -342,27 +343,9 @@ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falh
342
343
 
343
344
  **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ê.
344
345
 
345
- ### mutate
346
-
347
- **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o verificador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um cenário exige e o verificador não achou o teste que a morde.
348
-
349
- **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.
350
-
351
- **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`.
352
-
353
- **Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o verificador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado; rodada que cai é `inconclusive`. **Nunca a suíte inteira:** sem subconjunto utilizável (nenhum teste da feature nem citando os tipos mutados) o comando recusa antes de compilar (exit 2) e manda provar o cenário por leitura; subconjunto que some em runtime (base que não seleciona teste, ou cai) deixa os mutantes `inconclusive` com a mesma razão.
354
-
355
- **Nó `e2e: true`:** arquivo coberto **só** por nó de teste `e2e: true` é recusado (exit 2, nomeando mutante, arquivo e nó): a suíte testa o container do compose e o `mutate` recompila só localmente — todo mutante sobreviveria em falso. Com outro nó dono do arquivo, ele é o escolhido — mas, havendo qualquer nó `e2e: true` no config, só se algum arquivo de teste dele (com atributo de teste) citar um tipo declarado no arquivo mutado: o dono de um `.cs` é a suíte .NET, não a referência de projeto, e a unidade que só referencia a API daria sobrevivente em falso. Sem citação, recusa (exit 2) antes do snapshot. Nas duas recusas a saída prescreve o protocolo manual com o comando pronto do nó e2e, cada passo numa linha só encadeado por `&&` (nunca `;`): base verde antes do 1º mutante (`eval "$(morph-spec env <f>)" && morph-spec e2e up <f> && morph-spec heavy run -- <comando do nó>`), a mesma cadeia depois de cada edição, e restauração conferida por `git diff --quiet` seguida de um novo `e2e up` — registrado em `4-review`. Sem nó `e2e: true`, vale a regra do subconjunto acima.
356
-
357
- **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`.
358
-
359
- **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.
360
-
361
- **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`.
362
-
363
346
  ### e2e
364
347
 
365
- **`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.
348
+ **`morph-spec e2e up|status|down|reset <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.
366
349
 
367
350
  `--fresh` derruba com volumes (`down -v`), apaga o carimbo, reconstrói e reinicia os `e2e.hostServices` e re-roda o seed.
368
351
 
@@ -370,7 +353,9 @@ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falh
370
353
 
371
354
  `down [--volumes]` é best-effort e nunca bloqueia; derruba também os `e2e.hostServices`.
372
355
 
373
- **Serviços no host (`e2e.hostServices`).** App cujo build é caro dentro da VM do Docker roda como processo do host, nas portas do bloco, e o compose fica só com as dependências. Declaração em `.morph/config/config.json` (forma em `config.schema.json`):
356
+ `reset [--json]` devolve os **dados** do stack no ar a um estado conhecido entre os fluxos do verificador do Gate 3 — é a ferramenta de execução dele, entre um fluxo e o próximo. Com `e2e.resetCommand` no `config.json`, roda esse comando (mesmo env do seed: bloco de portas + `e2e.auth.envFile`); sem ele, recria vazios só os serviços do compose **sem** `build:` que montam um volume **nomeado** não `external` (`docker compose rm -sfv <svc>`, `docker volume rm` dos volumes deles, `docker compose up -d --wait <svc>` e a espera de saúde). Depois re-roda o seed (`e2e.auth`). Os serviços da app e os `e2e.hostServices` **não** reiniciam. Só re-rodar o seed não basta: o banco sujo guarda as linhas do fluxo anterior, e um seed que não é idempotente morre em chave duplicada. Na stack só de host não há volume: roda o `e2e.resetCommand` ou só o seed (`reset.mode: "none"`). Stack fora do ar (na só de host: algum serviço do host morto) ou sem compose nem `e2e.hostServices`: `status: "fail"`, exit 1, nada tocado. Nada a resetar e sem seed: `status: "skip"` com a razão, exit 0. `--json`: `{ feature, project, tree, status, reason, reset: { mode: "command"|"volumes"|"none", services, volumes }, auth: { status, reason } }`.
357
+
358
+ **Serviços no host (`e2e.hostServices`).** App cujo build é caro dentro da VM do Docker roda como processo do host, nas portas do bloco, e o compose fica só com as dependências. App **sem dependência** (sem banco, sem fila: dados no `localStorage`, só chamadas a APIs externas) não precisa de compose: declara só os `e2e.hostServices` e a stack é **só de host** — `e2e up`/`status`/`reset`/`down` e o nó do `verify` funcionam sem Docker (o carimbo de frescor é gravado do mesmo jeito). Compose entra quando há dependência a subir. Um `e2e.composeFile` configurado e ausente continua erro, com ou sem `e2e.hostServices`: a dependência que ele declara nunca subiria. Declaração em `.morph/config/config.json` (forma em `config.schema.json`):
374
359
 
375
360
  ```json
376
361
  "e2e": { "hostServices": [
@@ -393,7 +378,7 @@ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falh
393
378
 
394
379
  `${VAR}` em `env` e `health` vem do env da stack, depois do `process.env`; variável sem valor (ou vazia) recusa **antes** do build, nomeando a variável e o serviço.
395
380
 
396
- **Ordem** (`bringUpE2e`, a mesma no `e2e up` e no nó `e2e` do `verify`): `compose up --wait` → cada serviço na ordem do array (build → start → health) → seed (`e2e.auth`) → specs (só no verify). **Reuso:** PIDs, hora de criação e carimbo do código (o mesmo `HEAD` + status + diff da stack) ficam em `<git-common-dir>/morph-spec/e2e-host-<projeto>.json`; carimbo igual e health verde → `reused`; carimbo divergente, conjunto de serviços mudado ou `--fresh`/`--fresh-e2e` → todos são mortos, reconstruídos e reiniciados; health morto com carimbo igual → só aquele reinicia. **Falha** (build ≠ 0, timeout, processo que morre, health que nunca fica verde) → `e2e up` sai 1 e o nó `e2e` falha, com o nome do serviço e a cauda do log; os serviços subidos nesta rodada são mortos e o seed não roda. Os serviços **ficam no ar** depois do `up` e do `verify`, até `e2e down`, `finish`, `delete` ou `worktree remove`. Um processo só é morto se a hora de criação do PID vivo bate com a gravada: PID reciclado não é morto, só avisado. `e2e status --json` traz `hostServices: [{ name, pid, alive, health }]`.
381
+ **Ordem** (`bringUpE2e`, a mesma no `e2e up` e no nó `e2e` do `verify`): `compose up --wait` (pulado na stack só de host) → cada serviço na ordem do array (build → start → health) → seed (`e2e.auth`) → specs (só no verify). **Reuso:** PIDs, hora de criação e carimbo do código (o mesmo `HEAD` + status + diff da stack) ficam em `<git-common-dir>/morph-spec/e2e-host-<projeto>.json`; carimbo igual e health verde → `reused`; carimbo divergente, conjunto de serviços mudado ou `--fresh`/`--fresh-e2e` → todos são mortos, reconstruídos e reiniciados; health morto com carimbo igual → só aquele reinicia. **Falha** (build ≠ 0, timeout, processo que morre, health que nunca fica verde) → `e2e up` sai 1 e o nó `e2e` falha, com o nome do serviço e a cauda do log; os serviços subidos nesta rodada são mortos e o seed não roda. Os serviços **ficam no ar** depois do `up` e do `verify`, até `e2e down`, `finish`, `delete` ou `worktree remove`. Um processo só é morto se a hora de criação do PID vivo bate com a gravada: PID reciclado não é morto, só avisado. `e2e status --json` traz `hostServices: [{ name, pid, alive, health }]`.
397
382
 
398
383
  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.
399
384
 
@@ -409,7 +394,9 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
409
394
 
410
395
  **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo do piso do tipo do comando, espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo).
411
396
 
412
- **Piso de RAM por tipo** (`heavy.minFreeMemGbByKind`, padrão `unit` 1,5 · `build` 4 · `e2e` 4 GB): `unit` = vitest/jest, `npm|pnpm|yarn test`, `npm run test[:x]` e o nó `tests` do verify; `e2e` = playwright, `docker compose up`, script com `e2e` no nome, o nó `e2e` e o `e2e up`; `build` = o resto (`dotnet build|test|publish` — o `dotnet test` compila —, `next build`, `docker build`, `mutate` e comando desconhecido). O `heavy run` e o hook do `gateBash` classificam pelo comando; o `verify` toma o slot com o piso do primeiro nó e, antes de um nó mais pesado (tests → e2e), espera de novo só pela RAM segurando o slot — estourar ali também é exit 3, sem stamp. `heavy.minFreeMemGb` (4) virou o fallback: **definido explicitamente**, vale para todo tipo que `minFreeMemGbByKind` não lista (`0` continua desligando a checagem em tudo); sem ele, cada tipo usa o seu padrão.
397
+ **Uma suíte de `verify` por vez:** o nó de testes de um `verify` espera também enquanto o `verify` de outra feature está no nó de testes, segurando o slot que já tinha; `heavy run` (o filtro de sub-agente), build e e2e não esperam por isso, só pelos slots. Estouro da espera sai `reason: "suite"` (exit 3).
398
+
399
+ **Piso de RAM por tipo** (`heavy.minFreeMemGbByKind`, padrão `unit` 1,5 · `build` 4 · `e2e` 4 GB): `unit` = vitest/jest, `npm|pnpm|yarn test`, `npm run test[:x]` e o nó `tests` do verify; `e2e` = playwright, `docker compose up`, script com `e2e` no nome, o nó `e2e` e o `e2e up`; `build` = o resto (`dotnet build|test|publish` — o `dotnet test` compila —, `next build`, `docker build` e comando desconhecido). O `heavy run` e o hook do `gateBash` classificam pelo comando; o `verify` toma o slot com o piso do primeiro nó e, antes de um nó mais pesado (tests → e2e), espera de novo só pela RAM segurando o slot — estourar ali também é exit 3, sem stamp. `heavy.minFreeMemGb` (4) virou o fallback: **definido explicitamente**, vale para todo tipo que `minFreeMemGbByKind` não lista (`0` continua desligando a checagem em tudo); sem ele, cada tipo usa o seu padrão.
413
400
 
414
401
  **Ocupado por RAM** (`verify`, `e2e up`, `heavy run`): a mensagem nomeia o piso e a chave (`heavy.minFreeMemGbByKind.<tipo>`) e lista as stacks e2e de pé deste repo (`docker compose ls`, prefixo `morph-`, compose sob a raiz principal — de todas as worktrees). Feature já arquivada (`.morph/archive/<f>` sem `.morph/features/<f>`) vem com `morph-spec e2e down <f>`; stack sem feature, com `docker compose -p <nome> down`. Docker fora ou lento (10 s) = lista vazia, nunca erro.
415
402
 
@@ -425,7 +412,7 @@ Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda
425
412
 
426
413
  `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).
427
414
 
428
- **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.
415
+ **Bash cru (`heavy.gateBash`, padrão `true`):** a sessão roda build e teste quando quer, e um `dotnet build`/`dotnet test`/`npm test` 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.
429
416
 
430
417
  `heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`.
431
418
 
@@ -435,7 +422,7 @@ Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda
435
422
 
436
423
  `run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula PROCESSOS do contrato de dispatch (`heavy run`) segue valendo para eles.
437
424
 
438
- **Teto de teste do sub-agent (`test-budget`, sempre ligado, antes do `heavy-gate`):** só Bash com `agent_id` (o orquestrador nunca é contado) e com `.morph/` presente. Rodada = comando de teste (`testRunKind`: `dotnet test|vstest`, `vitest`, `jest`, `playwright test`, `npm|pnpm|yarn|bun test`, inclusive dentro de `heavy run --`) ou `morph-spec verify`; listagem (`--list`) não conta. Nega `verify` sem `--filter` (é do orquestrador), a 4ª rodada do mesmo sub-agent (`verify --filter` conta; a que sai com exit 3, fila cheia, não) com a instrução de gravar `PARCIAL` e devolver (a retomada pelo orquestrador, por `SendMessage` com o id do agente, devolve as 3 rodadas), e qualquer teste ou `verify` de `morph-verifier`/`morph-reviewer`/`morph-security` — `morph-spec mutate` passa. O contador mora em `.morph/memory/test-runs/<agent_id>.json`, gravado no `PostToolUse` e no `PostToolUseFailure`; qualquer erro é fail-open.
425
+ **Teste dos agentes do Gate 3 (`test-budget`, sempre ligado, `PreToolUse` antes do `heavy-gate`):** só Bash com `agent_id` cujo `agent_type` é `morph-verifier`, `morph-reviewer` ou `morph-security`, com `.morph/` presente; a sessão principal e qualquer outro sub-agent rodam o que quiserem. Nega `morph-spec verify` sem `--filter` (o verify cheio é a prova do gate, rodado pela sessão) e a suíte inteira (`testRunKind`: `dotnet test|vstest`, `vitest`, `jest`, `playwright test`, `npm|pnpm|yarn|bun test`, inclusive dentro de `heavy run --`). **Libera** o teste filtrado — o verificador executa o teste do cenário: `verify --filter`, `dotnet test --filter|--tests`, `-t`/`--testNamePattern`/`--grep`/`-g`, ou um argumento com forma de caminho de teste (`/`, `.test`/`.spec`, `.js|.ts|…`; o valor de `--config`/`--project`/`--reporter` não conta). Sem contador, sem estado gravado; qualquer erro é fail-open.
439
426
 
440
427
  **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.
441
428
 
@@ -499,35 +486,27 @@ Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*
499
486
 
500
487
  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.
501
488
 
502
- **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`).
503
-
504
489
  **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.
505
490
 
506
491
  ### report-check
507
492
 
508
- **`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.
493
+ **`morph-spec report-check <feature> [--before-eval] [--json]`** — o estado do implement e dos relatórios do Gate 3. Sempre exit 0; sem `checks.json` recusa (exit 1) nomeando o `plan-check`.
509
494
 
510
- 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-blocked` (relatório com `BLOQUEADO` na 1ª linha — decisão que não é do agente, com a pergunta e as opções) e ação `ask-human`: NÃO re-despache; decida pelo plano/ADR ou pergunte por `AskUserQuestion` e retome o mesmo agente com a resposta (a fatia fica fora das prontas e o `advance` do implement recusa); `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.
495
+ `implement: { total, done, pending, blocked, legacy, warnings }` sai de `3-implement/report.md` — a tabela `| Cn | status | evidência |` (`pendente`, `feito`, `bloqueado`), régua única em `hooks/shared/report-progress.js`; status fora do vocabulário conta como pendente. Feature anterior à 10.0, sem `report.md`, conta pelos relatórios de fatia (`legacy: true`). `reviews: { verifier, reviewer, security }`, cada um `{ exists, complete, partial, verdict }`: `complete` = veredito legível e sem `PARCIAL` na 1ª linha; `partial` = o agente bateu no teto de turnos — despache o mesmo papel de novo dizendo o que já está no relatório.
511
496
 
512
- **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`.
497
+ **Bloco `tree`**, em toda saída e sem mudar o 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), `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.
513
498
 
514
- 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.
499
+ **`--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`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita 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`.
515
500
 
516
- **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.
517
-
518
- **`--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`.
519
-
520
- 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.
521
-
522
- **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.
501
+ ### plan-check
523
502
 
524
- ### scout-check
503
+ **`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; 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) — e `2-plan/plan.html`, a visão só leitura que o gate do plano abre no navegador: cenários com prova/risco/degrau, padrões assumidos numerados e, com `## Estados` (opcional, `- origem → destino: gatilho` por linha), a máquina de estados desenhada. **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); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`, risco provado só por `leitura`, ADR sem `**Alcance:** feature|projeto`, `## Fatias` que sobrou da 9.x (não é lida). 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. Plano extraído antes das regras de `risco:` e de mídia não passa a falhar por elas. Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a. Com o plano válido, acrescenta ao `config.json` a stack que os arquivos mostram e instala em `.claude/rules/` a regra de stack que falta (uma linha por regra, em stderr; `rulesAdded` no `--json`).
525
504
 
526
- **`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.
505
+ ### stack-set
527
506
 
528
- ### plan-check
507
+ **`morph-spec stack-set <stack> [--with <integrações>] [--json]`** — a stack que o usuário escolheu (`dotnet`, `nextjs` ou `dotnet+nextjs`) vai para `project.stack` do `config.json` (substitui, inclusive `unknown`); `--with neon,maf,docker` soma a `project.integrations`. Em seguida instala tudo o que a stack filtra, pelo mesmo caminho do `init`: rules de domínio em `.claude/rules/`, commands e skills. Tag desconhecida, `config.json` ausente ou ilegível, ou `docker` sem compose: exit 1 nomeando o motivo, nada gravado. A recomendação é sempre a stack completa do Morph, `dotnet+nextjs --with neon` (standard `architecture-stack-baseline`); recorte é escolha do usuário. Projeto vazio: o `init` pergunta (ou `init --stack <s> --with <i>`); sem terminal, a `morph-plan` pergunta antes de planejar.
529
508
 
530
- **`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.
509
+ **Por quê:** as rules carregam no início de uma sessão do Claude Code (medido no 2.1.312: rule criada no meio da sessão não vale para os arquivos seguintes). Num projeto novo, só a stack declarada antes da sessão que implementa entrega as regras de domínio a tempo; o `plan-check`, o `verify` e o `update` só alcançam a sessão seguinte.
531
510
 
532
511
  ### stack-scan
533
512
 
@@ -550,9 +529,9 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
550
529
  "tests": [
551
530
  { "id": "dotnet", "runner": "dotnet", "cwd": ".",
552
531
  "command": "dotnet test tests/App.Tests/App.Tests.csproj --nologo -v q --no-build",
553
- "build": ["tests/App.Tests/App.Tests.csproj"], "expectedCount": 3057, "timeoutMs": 600000 },
532
+ "build": ["tests/App.Tests/App.Tests.csproj"], "timeoutMs": 600000 },
554
533
  { "id": "vitest", "runner": "vitest", "cwd": "catalog-admin",
555
- "command": "npx vitest run --reporter=default", "expectedCount": 3082 },
534
+ "command": "npx vitest run --reporter=default" },
556
535
  { "id": "api-integration", "runner": "dotnet", "cwd": ".",
557
536
  "command": "dotnet test tests/App.Integration.Tests/App.Integration.Tests.csproj -v q", "e2e": true }
558
537
  ]
@@ -566,9 +545,9 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
566
545
  | `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
567
546
  | `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 |
568
547
  | `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 |
569
- | `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
570
- | `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 |
571
- | `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 |
548
+ | `expectedCount` | não | Baseline, só nos runners de contagem estável (`node-test`, `evals`); nos outros é ignorada e o `--record-baseline` a apaga. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
549
+ | `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
550
+ | `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
572
551
  | `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 |
573
552
  | `linuxImage` | não | Imagem do container deste nó sob `--linux` |
574
553
 
@@ -704,6 +683,11 @@ reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa**
704
683
  — um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
705
684
  nó verde**, que é o único lugar onde ele pode existir.
706
685
 
686
+ **Baseline só onde a contagem fica parada.** `dotnet`, `vitest`, `jest` e `playwright` crescem a cada
687
+ fatia: a baseline deles só avisava a alta ou barrava a limpeza deliberada, e teste apagado já aparece no
688
+ diff que o Gate 3 lê. Nesses runners não há baseline; vale o **piso zero** — nó verde que executou `0`
689
+ testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou de rodar).
690
+
707
691
  **Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
708
692
  os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
709
693
  antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
@@ -716,10 +700,10 @@ a partir do binário do último build bom e reportava verde. `--no-build` contin
716
700
  sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógio do caminho quente.
717
701
 
718
702
  **O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
719
- escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
703
+ escrita: mede cada nó numa execução real e grava `expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (apaga o dos demais, listados como `removed`; escrita
720
704
  atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
721
705
  vermelha (fora o `count-drop` com exit 0, abaixo), execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
722
- afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
706
+ afirmação sobre a outra metade que ninguém fez), contagem desconhecida num nó `node-test`/`evals`, e **sem `config.json` ou sem
723
707
  `project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
724
708
  fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
725
709
  recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
@@ -732,14 +716,14 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
732
716
 
733
717
  | Momento | Comando | Por quê |
734
718
  | --- | --- | --- |
735
- | 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 |
719
+ | Sessão que implementa | à vontade — filtro ou suíte —, pela fila `heavy` (`heavy.gateBash` ligado por padrão) | Implementar e conferir é dela; a prova do gate continua sendo o `verify` sem filtro |
736
720
  | 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 |
737
721
  | 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 |
738
722
  | 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 |
739
723
 
740
724
  > **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
741
725
  > corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
742
- > relógios que o CLI de fato tem: build **120 000 ms**, tests **120 000 ms por projeto de teste
726
+ > relógios que o CLI de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
743
727
  > encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
744
728
  > que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
745
729
  > **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
@@ -773,7 +757,7 @@ Falhas do `--show` saem 1: id inexistente (`Standard "<id>" não existe.` com at
773
757
 
774
758
  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"**:
775
759
  - **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;
776
- - **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
760
+ - **fora do projeto** — `~/.claude/statusline.js` (Node, mesmo comando em todo SO; apaga o `statusline.py`/`.sh` de instalações antigas) e a mesclagem de `~/.claude/settings.json`;
777
761
  - **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.
778
762
 
779
763
  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`.
@@ -786,9 +770,9 @@ O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bl
786
770
 
787
771
  Grafo de símbolos extraído localmente pelo [graphify](https://pypi.org/project/graphifyy/) (versão pinada **0.9.50**, AST via tree-sitter, **zero token**), servido atrás de uma camada de saneamento própria. **Tudo é fail-open:** sem Python, sem grafo, com grafo velho ou corrompido, o framework segue por grep — é assim que ele sempre funcionou, e nenhuma skill pode exigir o grafo (ADR-002).
788
772
 
789
- O ganho medido **não é recall**: contra o grep, o grafo não achou um único arquivo a mais. É precisão — em 12 símbolos do ProspectPRO, o grep aponta 1.092 arquivos onde existem 266 usos reais (58% de ruído), e o grafo ainda diz *que tipo* de relação é cada um. `IHandler` aparece em 193 arquivos no grep e tem **2** usos reais: é implementada por 98.
773
+ O ganho medido **não é recall**: contra o grep, o grafo não achou um único arquivo a mais. É precisão — em 12 símbolos de um projeto .NET+Next.js em produção, o grep aponta 1.092 arquivos onde existem 266 usos reais (58% de ruído), e o grafo ainda diz *que tipo* de relação é cada um. `IHandler` aparece em 193 arquivos no grep e tem **2** usos reais: é implementada por 98.
790
774
 
791
- **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.
775
+ **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 num projeto .NET em produção), 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.
792
776
 
793
777
  ### graph status
794
778
 
@@ -800,7 +784,7 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
800
784
 
801
785
  ### graph refresh
802
786
 
803
- **`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.
787
+ **`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; num projeto de 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.
804
788
 
805
789
  ### graph affected
806
790
 
@@ -810,6 +794,20 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
810
794
 
811
795
  **`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`.
812
796
 
797
+ ### graph diff
798
+
799
+ **`morph-spec graph diff [--base <ref>] [-p <path>] [--json]`**: a árvore de chamadas do que mudou, de `--base` (default: merge-base com a branch padrão) até o HEAD.
800
+ - **O que entra:** todo símbolo cujas linhas o diff tocou. Vai marcado `+` quando o arquivo é novo e `~` quando é alterado, com `arquivo:linha` e `[teste]` se for teste.
801
+ - **Como a árvore se monta:** os símbolos se ligam pelas chamadas entre eles (`calls`, `indirect_call`, `dispatches_to`). Raiz é quem não tem chamador alterado; o que já apareceu volta como `↑ já mostrado`, e um ciclo aparece como `↺ ciclo`.
802
+ - **Saída:** `--json` traz `roots`, `counts` (`added`, `modified`, `entries`, `files`), `unmapped` (arquivos sem símbolo tocado: import, config, doc, ou fora do `.graphifyignore`) e `uncommitted`.
803
+ - **Só o commitado:** o grafo é um retrato do HEAD. Mudança fora do commit não entra; os arquivos do diff que estão sujos saem em `uncommitted`.
804
+ - **Nunca lê grafo velho.** Exit 1 com `reason`:
805
+ - `no-graph`: não há grafo;
806
+ - `stale-graph`: o grafo não é deste HEAD, ou as regras do `.graphifyignore` mudaram;
807
+ - `graph-drift`: o carimbo bate, mas algum símbolo não está declarado na linha que o grafo aponta no HEAD (a extração leu arquivos sujos);
808
+ - `no-base`: não há base para o diff.
809
+ - **Recusado:** leia o diff cru.
810
+
813
811
  ### Refresh automático, worktree e testes no grafo
814
812
 
815
813
  **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.
@@ -820,7 +818,7 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
820
818
 
821
819
  **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.
822
820
 
823
- **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.
821
+ **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 (medido num projeto .NET: 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.
824
822
 
825
823
  **O que o resultado significa muda com a stack.** Em backend, blast radius é *quem chama*. Em frontend, o grafo é estrutura de módulos e não call graph (`imports: 78` contra `calls: 3` no LP_Polymorphism), então ali ele responde *quem importa*. Ler um como o outro leva a conclusão errada, e é por isso que as personas dizem isso no próprio texto.
826
824
 
@@ -832,7 +830,7 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
832
830
 
833
831
  ### telemetry
834
832
 
835
- **`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.
833
+ **`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, sub-agents despachados + 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.
836
834
 
837
835
  ### cost
838
836
 
@@ -850,11 +848,11 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
850
848
  | 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) |
851
849
  | 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 |
852
850
  | 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 |
853
- | teste fora do verify | `testOutsideVerify`: sub-agente rodando `morph-spec verify` sem `--filter`, rodadas de teste de implementador da 3ª em diante (por agente) e teste/verify de agente de review, em execuções e minutos sobre o total cronometrado; exit 3 (fila cheia) não conta. Review por persona `morph-*` aparece à parte: é o que o hook `test-budget` nega |
851
+ | teste fora do verify | `testOutsideVerify`: teste/verify de agente do Gate 3, em execuções e minutos sobre o total de teste+verify cronometrado (a sessão roda teste à vontade e entra só no total); exit 3 (fila cheia) não conta. Review por persona `morph-*` aparece à parte: é o que o hook `test-budget` nega |
854
852
  | suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
855
853
  | git | ocorrências de `git status` e `git diff` na janela |
856
854
  | reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
857
- | gates | por gate, dos eventos `Gate`: `decisions`, `paused`, `autoPassed`, `unapproved`, e o destino de cada pausa — `approvedAfterPause` (o humano aprovou sem nova decisão: o risco foi aceito como estava) ou `reworkedAfterPause` (uma decisão seguinte já não pausou pelo mesmo sinal: o sinal levou a mudança). `bySignal` dá os três por sinal (`signal` de `shouldPauseGate`, ou `manual`). Rodar o `gate-check` de novo sobre a mesma pausa não conta como retrabalho. Sinal com muito `approvedAfterPause` e pouco `reworkedAfterPause` é candidato a afrouxar — a decisão é humana, o número só a informa |
855
+ | gates | por gate, dos eventos `Gate`: `decisions`, `paused`, `pausedForHuman` (pausas com `resolver: human`; evento anterior ao campo conta como humano, e `pausedUnattributed` diz quantas dessas vieram sem `resolver`: desconte-as para medir só o depois), `autoPassed`, `unapproved`, e o destino de cada pausa — `approvedAfterPause` (o humano aprovou sem nova decisão: o risco foi aceito como estava) ou `reworkedAfterPause` (uma decisão seguinte já não pausou pelo mesmo sinal: o sinal levou a mudança). `bySignal` dá `paused`, `pausedForHuman` e os dois destinos por sinal (`signal` de `shouldPauseGate`, ou `manual`); o dashboard (Atividade, card "Pausas por gate") desenha esses números. Rodar o `gate-check` de novo sobre a mesma pausa não conta como retrabalho. Sinal com muito `approvedAfterPause` e pouco `reworkedAfterPause` é candidato a afrouxar — a decisão é humana, o número só a informa |
858
856
 
859
857
  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.
860
858