@polymorphism-tech/morph-spec 8.32.0 → 8.34.2

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 (176) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +26 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +71 -68
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +218 -39
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +37 -4
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.32.0",
3
+ "version": "8.34.2",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -186,4 +186,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
186
186
 
187
187
  ---
188
188
 
189
- *morph-spec v8.32.0 by Polymorphism Tech*
189
+ *morph-spec v8.34.2 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.32.0 by Polymorphism Tech*
252
+ *morph-spec v8.34.2 by Polymorphism Tech*
@@ -60,7 +60,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
60
60
  | `retype <f> <tipo>` | Muda o workType mid-flight; bloqueado após o Gate 3 |
61
61
  | `finish <f> --pr\|--merge` | Encerramento único: arquiva, promove use cases, desmonta worktree. Integra só no `--merge`; `--pr` abre PR e fica `aguardando-merge` até alguém mergear |
62
62
  | `archive <f>` | Arquiva sem integrar (legadas/faxina) |
63
- | `worktree setup\|link\|remove\|list` | Ciclo do worktree; `remove` é a única remoção segura |
63
+ | `worktree setup\|provision\|link\|remove\|list` | Ciclo do worktree; `provision` = isolamento de task; `remove` é a única remoção segura |
64
64
  | `branch prune [--dry-run]` | Apaga branch já integrada, inclusive squash-merge; nunca a com PR aberto |
65
65
  | `handoff <f>` | Regenera o documento de retomada da feature |
66
66
  | `fleet` | Visão agregada de toda feature ativa (raiz + worktrees) |
@@ -109,6 +109,6 @@ Drift entre `agents.json` e os `.md` gerados é detectado por `morph-spec doctor
109
109
 
110
110
  ## Regra de precedência de skills
111
111
 
112
- Prefira `morph-*` a `superpowers:*` equivalentes (`brainstorming`→`morph-brainstorming`, `writing-plans`→`morph-plan`, `executing-plans`→`morph-apply`, `test-driven-development`→seguir `mandate.md`, TDD embutido): `morph-*` aplica Gates e paths corretos de `.morph/`; `superpowers:*` é genérico, sem esse contexto.
112
+ Prefira `morph-*` a `superpowers:*` equivalentes (`brainstorming`→`morph-brainstorming`, `writing-plans`→`morph-plan`, `executing-plans`→`morph-apply`, `test-driven-development`→seguir `mandate.md`, TDD embutido, `using-git-worktrees`→`morph-spec worktree setup|provision`): `morph-*` aplica Gates e paths corretos de `.morph/`; `superpowers:*` é genérico, sem esse contexto.
113
113
 
114
114
  *MORPH-SPEC v5 by Polymorphism Tech*
@@ -118,11 +118,10 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
118
118
 
119
119
  - **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (setup recusa, exit 3).
120
120
  - `feature.json` viaja **commitado na branch** `morph/{f}`; a cópia da raiz é esqueleto do nascimento, nunca mais atualizado — leitores (`list`/`handoff`/`fleet`) seguem a branch, mutadores recusam gravar fora da árvore dona (detalhes: CLI.md). `state.json` do worktree é índice derivável — self-heal, nunca sincronizado à mão.
121
- - Infra compartilhada (`node_modules`, `.claude`, `.morph/framework`) linkada por junction; `memory/`/`logs/` são locais de cada worktree. Faltou infra (worktree aberto com `git worktree add` puro) → `morph-spec worktree link --all`.
122
- - **Só o morph remove worktree.** Remoção genérica — `git worktree remove` (com OU sem `--force`), `rm -rf`, `Remove-Item -Recurse`, explorador/IDE — **atravessa a junction** e apaga o conteúdo real compartilhado da raiz (`.claude`/`node_modules`/`.morph/framework`), não só o link, saindo com sucesso. Foi assim que uma raiz teve `.claude/` e `.morph/framework/` esvaziados. Só `morph-spec` desmonta a junction ANTES de remover (PROVE-or-DECLINE): **fechar a feature** = `morph-spec finish <f> --pr|--merge`; **só desmontar o worktree** (feature segue aberta) = `morph-spec worktree remove <f>` — ou `--target <path>` para worktree sem feature. O hook `block-destructive-worktree-removal` barra a versão crua, mas a regra vale antes dele.
123
- - **Ambiente separado (portas):** setup aloca **bloco de portas determinístico** por feature em `.morph/worktree.env` (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` se dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as padrão (3000/5000) pertencem à raiz. Local, git-invisível; SessionStart injeta as portas. Override: `config.json → fleet.basePort/blockSize`.
124
- - **Banco de dados:** compartilhado por padrão; isolar via Neon MCP: branch de DB `morph/{f}` + connection string em `.morph/worktree.env` (sem Neon, prefixe schema/DB pelo nome da feature). Migration destrutiva → integre antes de abrir worktrees irmãos.
125
- - **Limitação conhecida:** `node_modules` compartilhado (junction) — dependência mudada afeta os irmãos; integre (`finish`) antes de seguir em paralelo.
121
+ - **Só o morph cria worktree:** `worktree setup <f>` (feature) ou `worktree provision <n>` (isolamento de task, branch `morph-task/<n>`). `git worktree add` cru é bloqueado; `EnterWorktree`/`-w`/`isolation:"worktree"` passam pelo hook `WorktreeCreate` e caem em `worktrees/`. Fora de `worktrees/` = criado sem o morph (`doctor --worktrees` acusa). Infra (`node_modules`, `.claude`, `.morph/framework`) por junction; faltou → `worktree link --all`.
122
+ - **Só o morph remove worktree.** Remoção genérica — `git worktree remove` (com OU sem `--force`), `rm -rf`, `Remove-Item -Recurse`, explorador/IDE — **atravessa a junction** e apaga o conteúdo real da raiz (`.claude`/`node_modules`/`.morph/framework`), saindo com sucesso (já esvaziou uma raiz). Só `morph-spec` desmonta a junction ANTES de remover (PROVE-or-DECLINE): **fechar a feature** = `morph-spec finish <f> --pr|--merge`; **só desmontar o worktree** = `morph-spec worktree remove <f>` — ou `--target <path>` para worktree sem feature. O hook barra a versão crua, mas a regra vale antes dele.
123
+ - **Ambiente dedicado:** setup escreve `.morph/worktree.env` (`PORT`, `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME=morph-{f}`); cada worktree sobe o **seu** compose. O hook `PreToolUse:Bash` injeta as variáveis em dev server/compose; fora do Claude, `eval "$(morph-spec env)"`. Porta fixa ou `container_name` no compose **reprova o nó e2e** em worktree. `finish` derruba o stack **com volumes** (`--keep-volumes` preserva). Neon branch opcional, credencial em `e2e.auth.envFile`, nunca em `worktree.env`. Migration destrutiva → integre antes de abrir irmãos.
124
+ - **Limitação:** `node_modules` é junction compartilhada — dependência mudada afeta os irmãos; integre antes de seguir em paralelo.
126
125
  - **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — retomada comprimida, regenerável (`morph-spec handoff <f>`, resolve a árvore dona sozinho), **aponta** para os artefatos em vez de copiá-los. Regenerado em `setup`/`create --worktree`/`advance`/`approve`. **Leia-o antes de re-derivar contexto à mão** — nunca edite manualmente.
127
126
  - **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira); `worktree list` é a visão enxuta por worktree.
128
127
  - **Ownership de arquivos:** sem lock runtime — colisão resolve **em plan-time** (`dag` detecta overlap de `outputs`, recomenda `isolation: "worktree"`) e **por construção** (uma feature = um worktree = um escritor). Arquivos compartilhados convergem no merge do `finish`; conflito ali é escopo sobreposto, não bug.
@@ -138,6 +138,7 @@
138
138
  "standards": [
139
139
  "backend-integrations-hangfire-hangfire-jobs",
140
140
  "infrastructure-docker-coolify-deploy",
141
+ "infrastructure-docker-local-compose-isolation",
141
142
  "backend-dotnet-scrutor",
142
143
  "ai-agents-observability-patterns"
143
144
  ],
@@ -1,68 +1,71 @@
1
- # morph-spec CLI — Referência Completa
2
-
3
- > Referência completa (flags, semântica, exemplos) dos comandos `morph-spec` usados pela LLM em vez de editar o estado à mão. Resumo rápido em `.morph/framework/CLAUDE.md`; racional dos gates em `.morph/framework/MORPH.md` §5, dispatch de sub-agents em §6, worktree/fleet em §2.2.
4
-
5
- ---
6
-
7
- ## Gates e estado
8
-
9
- - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
10
- - **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
11
- - **`morph-spec score <feature> <task> <score> [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
12
- - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, 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`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
13
- - **`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.
14
- - **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
15
-
16
- ## Features
17
-
18
- - **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
19
- - **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
20
- - **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
21
- - **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
22
-
23
- ## Worktree / Fleet
24
-
25
- - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
26
- - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
27
- - **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
28
- - **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
- - **`morph-spec worktree remove [feature] [--target <path>] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). 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.
30
- - **`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.
31
- - **`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`.
32
- - **`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.
33
- - **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
34
-
35
- ## Validação
36
-
37
- - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `--mermaid` nunca é bloqueado.
38
- - **`morph-spec verify <feature> [task] [--json] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
39
- - **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
40
- - **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias.
41
- **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
42
- **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
43
- Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado).
44
- - **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto.
45
- - **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Protocolo completo em MORPH.md §6.
46
-
47
- ## Grafo de codebase
48
-
49
- 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).
50
-
51
- 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.
52
-
53
- **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.
54
-
55
- - **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD` — e importa porque o hook `post-commit` do graphify rebuilda **em background**: logo depois de um commit, o grafo ainda é o do commit anterior. `--json` devolve `{ available, path, nodes, links, freshness: {state}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous} }`.
56
- - **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois. Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace.
57
- - **`morph-spec graph refresh [-p <path>] [--json]`** — atualização incremental (`graphify update`), 2 a 3× mais rápida que o build graças ao cache por hash de conteúdo.
58
- - **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores em projeto de teste. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
59
- - **`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`), então dá para ver exatamente o que o saneamento recuperou.
60
-
61
- **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.
62
-
63
- O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está registrado em `framework/mcp/registry.json` para exploração interativa e consulta cross-repo — mas ele devolve o grafo **cru**, sem saneamento. Para qualquer decisão, prefira os comandos acima.
64
-
65
- ## Observabilidade
66
-
67
- - **`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` em todo tool call e todo UserPromptSubmit.
68
- - **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não eram observáveis por hook até o evento `SessionUsage` — quando presente no stream, `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens` são somados e exibidos à parte como "tokens reais", nunca misturados com a proxy de bytes). `--all-trees` agrega `.morph/logs/events.jsonl` de **toda árvore git** que tiver um (não só a corrente) — reusa a mesma enumeração de worktree do `doctor --worktrees`/`fleet`; mostra um resumo por árvore + o total combinado (`--by` aplica ao agregado combinado). Use antes/depois de mudar skill ou fluxo para medir o efeito.
1
+ # morph-spec CLI — Referência Completa
2
+
3
+ > Referência completa (flags, semântica, exemplos) dos comandos `morph-spec` usados pela LLM em vez de editar o estado à mão. Resumo rápido em `.morph/framework/CLAUDE.md`; racional dos gates em `.morph/framework/MORPH.md` §5, dispatch de sub-agents em §6, worktree/fleet em §2.2.
4
+
5
+ ---
6
+
7
+ ## Gates e estado
8
+
9
+ - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
10
+ - **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
11
+ - **`morph-spec score <feature> <task> <score> [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
12
+ - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, 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`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
13
+ - **`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.
14
+ - **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
15
+
16
+ ## Features
17
+
18
+ - **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
19
+ - **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
20
+ - **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
21
+ - **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
22
+
23
+ ## Worktree / Fleet
24
+
25
+ - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
26
+ - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
27
+ - **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
28
+ - **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
+ **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
30
+ **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`. 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).
31
+ - **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks).
32
+ - **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). 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.
33
+ - **`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.
34
+ - **`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`.
35
+ - **`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.
36
+ - **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
37
+
38
+ ## Validação
39
+
40
+ - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `--mermaid` nunca é bloqueado.
41
+ - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
42
+ - **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
43
+ - **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias.
44
+ **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
45
+ **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
46
+ Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado).
47
+ - **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto.
48
+ - **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Protocolo completo em MORPH.md §6.
49
+
50
+ ## Grafo de codebase
51
+
52
+ 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).
53
+
54
+ 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.
55
+
56
+ **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.
57
+
58
+ - **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD` — e importa porque o hook `post-commit` do graphify rebuilda **em background**: logo depois de um commit, o grafo ainda é o do commit anterior. `--json` devolve `{ available, path, nodes, links, freshness: {state}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous} }`.
59
+ - **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois. Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace.
60
+ - **`morph-spec graph refresh [-p <path>] [--json]`** — atualização incremental (`graphify update`), 2 a 3× mais rápida que o build graças ao cache por hash de conteúdo.
61
+ - **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores em projeto de teste. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
62
+ - **`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`), então dá para ver exatamente o que o saneamento recuperou.
63
+
64
+ **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.
65
+
66
+ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está registrado em `framework/mcp/registry.json` para exploração interativa e consulta cross-repo — mas ele devolve o grafo **cru**, sem saneamento. Para qualquer decisão, prefira os comandos acima.
67
+
68
+ ## Observabilidade
69
+
70
+ - **`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` em todo tool call e todo UserPromptSubmit.
71
+ - **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não eram observáveis por hook até o evento `SessionUsage` — quando presente no stream, `inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens` são somados e exibidos à parte como "tokens reais", nunca misturados com a proxy de bytes). `--all-trees` agrega `.morph/logs/events.jsonl` de **toda árvore git** que tiver um (não só a corrente) — reusa a mesma enumeração de worktree do `doctor --worktrees`/`fleet`; mostra um resumo por árvore + o total combinado (`--by` aplica ao agregado combinado). Use antes/depois de mudar skill ou fluxo para medir o efeito.
@@ -8,7 +8,11 @@ Comprehensive hooks system for enforcing spec-driven development at the Claude C
8
8
  framework/hooks/
9
9
  ├── claude-code/ # Claude Code native hooks
10
10
  │ ├── core/
11
- │ │ └── worktree-context.js # SessionStart: worktree feature identity / active-feature summary
11
+ │ │ ├── worktree-context.js # SessionStart: worktree feature identity / active-feature summary
12
+ │ │ ├── worktree-create.js # WorktreeCreate: governed tree via `morph-spec worktree setup|provision`
13
+ │ │ ├── worktree-remove.js # WorktreeRemove: safe teardown via `worktree remove --if-clean`
14
+ │ │ ├── block-destructive-worktree-removal.js # PreToolUse(Bash): raw removal follows junctions out
15
+ │ │ └── block-raw-worktree-add.js # PreToolUse(Bash): raw `git worktree add` is ungoverned
12
16
  │ ├── user-prompt/
13
17
  │ │ └── enrich-prompt.js # Context-aware prompt enrichment
14
18
  │ ├── pre-tool-use/
@@ -46,6 +50,10 @@ framework/hooks/
46
50
  | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to spec files after approval; for tasks.json validates the mutable-field diff, the status enum, and requires a `notes` when `outputs` diverge from the plan |
47
51
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
48
52
  | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
53
+ | **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
54
+ | **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
55
+ | **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), honours `worktree.baseRef:"head"`, prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
56
+ | **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
49
57
  | **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success. Also touches `.morph/memory/build-dirty.json` on every source-file edit — the write side of validate-completion.js's Stop-hook build-dirty gate |
50
58
  | **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
51
59
  | **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md + taskScores on every tasks.json write — the skills never write traces by hand |
@@ -115,6 +123,8 @@ All Claude Code hooks import from `framework/hooks/shared/`:
115
123
  | `hook-response.js` | `block(reason)`, `approve(context)`, `injectContext(text)`, `pass()` |
116
124
  | `stdin-reader.js` | `readStdin()` — Promise-based stdin JSON reader |
117
125
  | `telemetry-logger.js` | `appendTelemetryEvent()`, `readTelemetryEvents()` — append-only JSONL event stream (`.morph/logs/events.jsonl`) |
126
+ | `morph-cli.js` | `runMorphSpec(args, { cwd, timeoutMs, env })` — spawns the morph-spec CLI from a hook (`MORPH_SPEC_BIN` → `morph-spec` on PATH → `npx --no-install`); `unavailable: true` when none resolves, never a throw. Hooks may never import from `src/` |
127
+ | `command-tokens.js` | `tokenize(command)` — quote-aware shell tokenizer shared by the Bash guards (a hook must never import a sibling hook: its standalone tail would fire) |
118
128
 
119
129
  `injectContext(text)` is how `worktree-context.js` (and any other SessionStart hook) delivers context: it emits `{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": text } }` on stdout. Claude Code only honors additional context nested under `hookSpecificOutput` — a bare top-level field is ignored.
120
130
 
@@ -4,6 +4,7 @@ import { resolve, isAbsolute } from 'path';
4
4
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
5
5
  import { stateExists } from '../../shared/state-reader.js';
6
6
  import { scanForBlockers } from '../../shared/reparse-scan.js';
7
+ import { tokenize } from '../../shared/command-tokens.js';
7
8
 
8
9
  /**
9
10
  * block-destructive-worktree-removal — the only defense that does not depend on
@@ -32,14 +33,10 @@ import { scanForBlockers } from '../../shared/reparse-scan.js';
32
33
  * nothing to traverse is not blocked for looking dangerous.
33
34
  */
34
35
 
35
- /** Split a shell-ish command into tokens, honoring single and double quotes. */
36
- function tokenize(command) {
37
- const tokens = [];
38
- const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
39
- let m;
40
- while ((m = re.exec(command)) !== null) tokens.push(m[1] ?? m[2] ?? m[3]);
41
- return tokens;
42
- }
36
+ // The tokenizer lives in shared/command-tokens.js — block-raw-worktree-add.js
37
+ // reads `git worktree add` off the same tokens, and a hook must never import a
38
+ // sibling hook (its standalone tail would fire in the importer's process).
39
+ export { tokenize };
43
40
 
44
41
  /**
45
42
  * The path a recursive-removal command would destroy, or null when this is not
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
+ import { stateExists } from '../../shared/state-reader.js';
4
+ // From shared/, never from the sibling hook: importing a hook module fires its
5
+ // standalone tail (stdin read + exit) inside THIS process.
6
+ import { tokenize } from '../../shared/command-tokens.js';
7
+
8
+ /**
9
+ * block-raw-worktree-add — PreToolUse (Bash) guard against `git worktree add`.
10
+ *
11
+ * A worktree created by raw git is a worktree morph cannot see: no infra
12
+ * junctions (`.claude`, `node_modules`, `.morph/framework` — so NO hooks run
13
+ * in there), no port block (its dev server collides with the root's), no
14
+ * feature/task identity (invisible to `fleet`, `doctor --worktrees`,
15
+ * `worktree list`), and a location git chose, not `<root>/worktrees/`. The
16
+ * sibling guard (block-destructive-worktree-removal) protects the way OUT;
17
+ * this one protects the way IN, by naming the two commands that build a
18
+ * governed tree:
19
+ *
20
+ * morph-spec worktree setup <feature> — a feature's tree
21
+ * morph-spec worktree provision <name> [--base <ref>] — a task-isolation tree
22
+ *
23
+ * DELIBERATELY NARROW: only the `add` subcommand. `list`, `prune`, `remove`
24
+ * (the sibling's business), `lock`, `unlock`, `move`, `repair` all pass. The
25
+ * `git` token may be followed by global options (`-C <dir>`, `--git-dir=<d>`,
26
+ * `-c k=v`) before `worktree`, and may sit anywhere in a chained command
27
+ * (`cd a && git worktree add b`) — every `git` occurrence is checked.
28
+ */
29
+
30
+ /** Global `git` options that take a separate value: `git -C <dir> worktree add`. */
31
+ const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
32
+
33
+ /**
34
+ * Does this command line invoke `git worktree add` anywhere?
35
+ * @param {string} command
36
+ * @returns {boolean}
37
+ */
38
+ export function isRawWorktreeAdd(command) {
39
+ const tokens = tokenize(command);
40
+ const lower = tokens.map((t) => t.toLowerCase());
41
+ for (let i = 0; i < lower.length; i++) {
42
+ if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
43
+ let j = i + 1;
44
+ // Skip global options between `git` and the subcommand.
45
+ while (j < lower.length && lower[j].startsWith('-')) {
46
+ if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2; // `-C dir` consumes its value
47
+ else j += 1; // `--git-dir=x`, `--no-pager`, …
48
+ }
49
+ if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
56
+ * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
57
+ */
58
+ export async function run(payload) {
59
+ try {
60
+ if (!stateExists()) return null;
61
+
62
+ const command = payload?.tool_input?.command || '';
63
+ if (!command || !isRawWorktreeAdd(command)) return null;
64
+
65
+ return blockResult(
66
+ 'Bloqueado: `git worktree add` cru cria um worktree que o morph-spec NÃO enxerga — sem as junctions de infra ' +
67
+ '(`.claude`, `node_modules`, `.morph/framework` → nenhum hook roda lá dentro), sem bloco de portas (o dev server ' +
68
+ 'colide com o da raiz), sem identidade (invisível para `fleet`, `doctor --worktrees` e `worktree list`) e fora de `<raiz>/worktrees/`.\n\n' +
69
+ 'Use o comando que constrói a árvore governada:\n' +
70
+ ' - feature: `morph-spec worktree setup <feature>`\n' +
71
+ ' - tarefa isolada (sem feature): `morph-spec worktree provision <nome> --base <ref>`\n\n' +
72
+ 'Os dois deixam a árvore em `<raiz>/worktrees/<nome>` com a infra completa; `list`/`prune`/`remove` do git não são afetados por este guard.'
73
+ );
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ await runStandalone(run);
@@ -37,6 +37,13 @@
37
37
  * mesmo grupo observam o MESMO evento real do Claude Code, então isto só
38
38
  * importa como desempate determinístico); na ausência de qualquer elo
39
39
  * declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
40
+ * - updatedInput (PreToolUse): um elo pode devolver
41
+ * `hookSpecificOutput.updatedInput` (ver `updatedInputResult` em
42
+ * hook-response.js) para reescrever o tool_input. O ÚLTIMO elo a devolver um
43
+ * vence (só um elo do grupo reescreve, por construção). `permissionDecision:
44
+ * 'allow'` + `updatedInput` só entram no resultado final quando algum elo
45
+ * reescreveu — um `allow` incondicional pularia o prompt de permissão de
46
+ * TODO Bash, que é exatamente o contrário de um guardrail.
40
47
  * - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
41
48
  * falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
42
49
  * ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
@@ -78,6 +85,7 @@ async function main() {
78
85
 
79
86
  const contexts = [];
80
87
  let eventName = null;
88
+ let updatedInput = null;
81
89
 
82
90
  for (const name of names) {
83
91
  let result = null;
@@ -97,6 +105,12 @@ async function main() {
97
105
  process.exit(0);
98
106
  }
99
107
 
108
+ const rewrite = result?.hookSpecificOutput?.updatedInput;
109
+ if (rewrite && typeof rewrite === 'object') {
110
+ updatedInput = rewrite; // last writer wins
111
+ eventName = eventName || result?.hookSpecificOutput?.hookEventName;
112
+ }
113
+
100
114
  const ctx = contextOf(result);
101
115
  if (ctx) {
102
116
  contexts.push(ctx);
@@ -107,16 +121,22 @@ async function main() {
107
121
  }
108
122
  }
109
123
 
110
- if (contexts.length === 0) {
124
+ if (contexts.length === 0 && !updatedInput) {
111
125
  process.exit(0);
112
126
  }
113
127
 
114
- console.log(JSON.stringify({
115
- hookSpecificOutput: {
116
- hookEventName: eventName || payload?.hook_event_name || 'PostToolUse',
117
- additionalContext: contexts.join('\n\n'),
118
- },
119
- }));
128
+ const hookSpecificOutput = {
129
+ hookEventName: eventName || payload?.hook_event_name || 'PostToolUse',
130
+ };
131
+ // `permissionDecision:'allow'` ONLY travels with a rewrite — an unconditional
132
+ // allow would skip the permission prompt for every command the chain sees.
133
+ if (updatedInput) {
134
+ hookSpecificOutput.permissionDecision = 'allow';
135
+ hookSpecificOutput.updatedInput = updatedInput;
136
+ }
137
+ if (contexts.length) hookSpecificOutput.additionalContext = contexts.join('\n\n');
138
+
139
+ console.log(JSON.stringify({ hookSpecificOutput }));
120
140
  process.exit(0);
121
141
  }
122
142