@polymorphism-tech/morph-spec 8.42.0 → 8.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CLAUDE.md +9 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +48 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +5 -4
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +1 -1
  10. package/framework/docs/CLI.md +10 -3
  11. package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
  12. package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
  13. package/framework/hooks/claude-code/core/heavy-release.js +34 -0
  14. package/framework/hooks/claude-code/core/worktree-context.js +21 -20
  15. package/framework/hooks/claude-code/core/worktree-create.js +4 -4
  16. package/framework/hooks/shared/agent-reap.js +89 -0
  17. package/framework/hooks/shared/e2e-check.js +3 -1
  18. package/framework/hooks/shared/graph-refresh.js +1 -9
  19. package/framework/hooks/shared/heavy-command.js +128 -0
  20. package/framework/hooks/shared/heavy-config.js +65 -0
  21. package/framework/hooks/shared/heavy-holders.js +85 -0
  22. package/framework/hooks/shared/heavy-slots.js +207 -0
  23. package/framework/hooks/shared/process-alive.js +8 -0
  24. package/framework/hooks/shared/worktree-root.js +93 -0
  25. package/framework/schemas/config.schema.json +80 -0
  26. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  27. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  28. package/framework/skills/core/morph-apply/SKILL.md +8 -3
  29. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  30. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  31. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  32. package/framework/skills/core/morph-implement/SKILL.md +9 -4
  33. package/framework/skills/core/morph-machine/SKILL.md +46 -0
  34. package/framework/skills/core/morph-media/SKILL.md +1 -1
  35. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +45 -16
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/PROVENANCE.json +10 -10
  42. package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
  43. package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
  44. package/package.json +1 -1
  45. package/src/commands/e2e/e2e.js +27 -8
  46. package/src/commands/mcp/mcp-setup.js +10 -9
  47. package/src/commands/project/doctor-procs.js +162 -0
  48. package/src/commands/project/doctor.js +42 -4
  49. package/src/commands/project/heavy.js +61 -0
  50. package/src/commands/project/init.js +1 -1
  51. package/src/commands/project/update.js +4 -0
  52. package/src/commands/project/worktree-reap.js +34 -0
  53. package/src/commands/project/worktree.js +78 -27
  54. package/src/commands/validation/mutate.js +200 -0
  55. package/src/commands/validation/verify.js +26 -1
  56. package/src/dashboard/scanner/features-scanner.js +4 -4
  57. package/src/dashboard/scanner/project-api.js +4 -4
  58. package/src/lib/detectors/claude-config-detector.js +30 -1
  59. package/src/lib/heavy/cleanup.js +70 -0
  60. package/src/lib/heavy/gate.js +96 -0
  61. package/src/lib/installers/mcp-installer.js +15 -2
  62. package/src/lib/mutation/adapters.js +90 -0
  63. package/src/lib/mutation/commands.js +60 -0
  64. package/src/lib/mutation/manifest.js +138 -0
  65. package/src/lib/mutation/plan.js +36 -0
  66. package/src/lib/mutation/report.js +66 -0
  67. package/src/lib/mutation/runner.js +351 -0
  68. package/src/lib/mutation/schemata.js +60 -0
  69. package/src/lib/mutation/snapshot.js +69 -0
  70. package/src/lib/mutation/subset.js +75 -0
  71. package/src/lib/mutation/trx.js +51 -0
  72. package/src/lib/procs/classify.js +91 -0
  73. package/src/lib/procs/list.js +124 -0
  74. package/src/lib/procs/mcp-config.js +103 -0
  75. package/src/lib/procs/prune.js +25 -0
  76. package/src/lib/procs/wslconfig.js +70 -0
  77. package/src/lib/projects/registry.js +6 -2
  78. package/src/lib/tasks/linux-runner.js +30 -3
  79. package/src/lib/tasks/test-runner.js +1 -1
  80. package/src/lib/validators/verify-runner.js +26 -8
  81. package/src/lib/worktree/agent-reaper.js +247 -0
  82. package/src/lib/worktree/dirs.js +3 -32
  83. package/src/lib/worktree/vscode-settings.js +113 -0
  84. package/src/lib/worktree/worktree-audit.js +49 -5
  85. package/src/lib/worktree/worktree-include.js +46 -0
  86. package/src/scripts/setup-infra.js +4 -0
  87. package/src/utils/hooks-installer.js +11 -4
package/CLAUDE.md CHANGED
@@ -149,7 +149,8 @@ When modifying framework components, verify the full scope of related files.
149
149
  | `src/lib/specs/screen-checklist.js` (checklist de tela, #129) | o sinal mora aqui (`collectScreenChecklist`) e tem QUATRO leitores: `gate-check.js` na `uiux` (`problems` → pausa) e no `review` (`uncovered`: item decidido sem linha na seção `## Checklist de tela` que `evaluator-report.js` lê — última ocorrência; tela toda "não se aplica" não deve nada), e `approve.js`/`advance.js` com assinatura `auto` (`uiuxAutoApprovalBlock`). Política só em `risk-detector.js`: `uiuxChecklistPause` é o predicado ÚNICO do gate-check pausar e do auto recusar — uma segunda regra no advance fez approve e advance discordarem da mesma feature — e `gateDecisions.uiux.pause === false` destrava os dois só para o mockup que a decisão viu: `persistGateDecision` (`gate-decision.js`, o escritor único de `gateDecisions`) grava `mockupsHash` e `uiuxAutoApprovalBlock` o compara com o arquivo atual — os dois leem `mockupsHash` daqui, e um segundo cálculo do hash (outra normalização de BOM/CRLF) faz toda decisão parecer de outro mockup. `--force` não destrava em nenhum dos dois. Mockup legado (`## S\d+:` e nenhuma `## Tela:`, o template anterior) é isento nas duas fases com `exempt` = `LEGACY_MOCKUP_EXEMPTION` à vista no `getRiskSignals`; qualquer outro heading é "nenhuma tela declarada", senão trocar o heading pularia o checklist (fixture: `test/fixtures/mockups/legacy-ui-mockups.md`, o template de main). Os rótulos dos itens casam com DOIS textos que o parser não vê, os dois com canário em `test/lib/specs/screen-checklist.test.js`: o template do prompt da `morph-uiux` §3 e `framework/templates/docs/ui-mockups.md`; mais o formato de linha que o prompt do avaliador (`morph-review` §4) e a rubrica `contracts.md` ("Contrato de tela") prescrevem. A `Superfície` é validada contra as chaves de `surfaces` da `ui-designer` (`knownSurfaces`) — é a chave que o `morph-plan` §5a usa para somar standards |
150
150
  | `src/lib/tasks/test-scope.js` (o recorte por task, #87) | Este DECIDE, `verify-runner.js` executa: `applyTaskScope` roda UMA vez em `runVerify`, antes do build, e `preDecideRun` (pula `out-of-scope`) e `judgeRun` → `judgeNarrowedRun` (nunca `compareTestCount`; zero ou desconhecido é vermelho) são a cópia única para host e `--linux` — o container recebe o recorte pelo `node.command`. Importa `effectiveGroups` (`test-nodes.js`), `extractCsprojTargets`/`isInside`/`canonicalPath` (`test-runner.js`), `normalizePath` (`traceability-check.js` — um normalizador só para `outputs`), `samePathCase` (`docker-audit.js` — só a CAIXA vem do disco: um path que atravessa symlink/junction fica como escrito, senão o arquivo muda de nó) e a introspecção .NET abaixo. `runs[].selection` sai só no `--json`: `nodeStatuses()` não o persiste. Piso `--minimum-expected-tests` de projeto MTP (#114): quando `floorOverride` o lê de UM lugar, o link recortado ganha `"-p:TestingPlatformCommandLineArguments=<valor com o piso em 1>"` (o `-p:` troca o valor inteiro — medido, sem recompilar); qualquer outra forma roda inteiro, e também o comando que já declara a propriedade (o `-p:` do recorte vem por último e ganharia) e o link que roda mais de um projeto (o `-p:` é global à invocação e trocaria os argumentos do outro). Forma de comando nova se lê e se MEDE (`test/fixtures/test-output/README.md`) antes de estreitar; o resto roda INTEIRO com motivo. Canários: `test/lib/tasks/test-scope.test.js`, o bloco `runVerify — task-scope recorte` de `test/lib/verify-runner.test.js`, `test/commands/verify-filter.test.js` e a prosa em `test/framework/verify-stack-docs-prose.test.js` |
151
151
  | `src/lib/tasks/dotnet-test-introspection.js` | Só LÊ (dividido de `test-scope.js`): o varredor C# que apaga literais (`blankCSharpLiterals`), o tripwire contra o texto CRU + o balanço de chaves (`csharpClasses`), `classNamesOf`, `dotnetTestMode` (lê o `global.json` por `jsoncToJson` de `framework/hooks/shared/comment-stripper.js`), o piso `--minimum-expected-tests` / `<VSTestTestCaseFilter>` do projeto, `floorOverride` (#114: o valor do `-p:` que rebaixa o piso a 1 e repassa o resto — só com UM `<TestingPlatformCommandLineArguments>` literal no csproj, sem atributo, num `<PropertyGroup>` sem atributo direto sob `<Project>` (`enclosingElements` anda nas tags: `Condition` no GRUPO, `<Target>` e `<Choose>` injetavam um filtro que o MSBuild não usaria), sem `TreatAsLocalProperty`, sem `<Import>`, sem a propriedade em nenhum outro arquivo que liga o projeto, sem piso no testconfig e sem `` $ @ % ; , & " ' ` \ ! `` no valor; `null` roda inteiro; `bindingMsbuildFiles` é a lista única de arquivos MSBuild que `msbuildDeclares` também lê — o `.csproj.user` e `Directory.Build.props/.targets/.rsp` + `Directory.Packages.props` até a raiz do DISCO, porque o MSBuild não para no repositório), `ignoresExitCode` (o MESMO scan de csproj/Directory.Build.*/testconfig: `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` desarma a prova do recorte MTP que resta quando o sumário é ilegível, o exit code — desde a #115 a contagem do sumário, pt-BR/en-US, prova primeiro, e o scan fica porque decide ANTES da rodada), o projeto de teste dono de um `.cs` (`dotnetTestProjectOf` — só `.cs`/`*.csproj`, via `nearestCsprojOf`), `nearestProjectOf` + `isDotnetTestProject` (qualquer arquivo; cobrem `.csproj`/`.fsproj`/`.vbproj` — `test-scope.js` roda o nó INTEIRO por QUALQUER arquivo não-`.cs` que o nó pode rodar cujo projeto mais próximo é de teste: `.feature`, `.verified.txt`, `.razor`, dado de teste), `isNonCSharpSource`/`isDotnetSource`, `readsLikeTestSource` (o marcador FROUXO no texto cru atrás de `unrecognisedTestFile` e do segundo braço do F2 — falso positivo só custa relógio) e `CS_TEST_ATTRIBUTE` (nome inteiro numa seção de atributo que FECHA — `, TestContext` num construtor e uma coleção `[\n Env.Test,\n]` compravam um verde; o resíduo sondado mora no JSDoc de `csharpClasses`). **Falha para o lado SEGURO:** varredura desbalanceada, literal sem fim, buraco de interpolação cortado ou palavra-chave de tipo que não virou declaração → o nó roda INTEIRO; um erro que ESTREITA roda menos testes e sai verde. `test-scope.js` reexporta `dotnetTestMode`/`classNamesOf`, e `test/lib/tasks/dotnet-test-introspection.test.js` pina que são as MESMAS funções. A garantia e o que ela NÃO cobre moram no JSDoc de `csharpClasses`: mexeu no varredor, reescreva-o junto e prove por mutação que o tripwire ainda morde |
152
- | `src/lib/tasks/linux-runner.js` | `verify-runner.js` (`runNodesInLinux` faz as MESMAS pré-decisões do laço local e julga o resultado pelo MESMO `judgeRun` — uma segunda cópia dessa regra é como container e host passam a discordar da mesma suíte), `framework/hooks/shared/e2e-check.js` (`defaultExec`/`isDockerAvailable` são reusados, nunca recriados), `src/lib/worktree/dirs.js` (nomes do contêiner de worktree nas exclusões do `tar`), `framework/schemas/config.schema.json` (`linuxImage`) e `test/lib/tasks/linux-runner.test.js` (o canário do bind mount: montar a árvore em `/work` é v9fs case-insensitive e não reproduz F35) |
152
+ | `src/lib/tasks/linux-runner.js` | `verify-runner.js` (`runNodesInLinux` faz as MESMAS pré-decisões do laço local e julga o resultado pelo MESMO `judgeRun` — uma segunda cópia dessa regra é como container e host passam a discordar da mesma suíte), `framework/hooks/shared/e2e-check.js` (`defaultExec`/`isDockerAvailable` são reusados, nunca recriados), `src/lib/worktree/dirs.js` (nomes do contêiner de worktree nas exclusões do `tar`), `framework/schemas/config.schema.json` (`linuxImage`) e `test/lib/tasks/linux-runner.test.js` (o canário do bind mount: montar a árvore em `/work` é v9fs case-insensitive e não reproduz F35). Os labels `morph-spec.verify`/`morph-spec.root` do `docker run` e `reapLabeledContainers` (`docker rm -f` por label, nunca por nome) são o que limpa o container efêmero sem tocar nos de outros projetos: `test/lib/tasks/linux-runner.test.js` pina o argv de `run` (filtra `args[0]==='run'`, porque o `ps`/`rm` do reap passam pelo mesmo `exec`). |
153
+ | `src/lib/mutation/**` (`morph-spec mutate`, a prova de mutação do Gate 3 em lote) | `src/commands/validation/mutate.js` (fina), `bin/morph-spec.js`, o bullet `mutate` de `framework/docs/CLI.md`, o bloco `MUTAÇÃO` do prompt do avaliador em `morph-review` §4 e `framework/evals/proof-rubrics/mutation-proof.md`, com os canários `test/framework/morph-review-skill.test.js` (o prompt nunca prescreve `os.utime` e dita o escopo do K), `test/lib/mutation/*` e `test/commands/mutate.test.js`. O mundo falso `test/helpers/mutation-world.js` reproduz o build INCREMENTAL do MSBuild (pula quando nenhuma fonte é mais nova que a DLL): sem essa propriedade o teste da armadilha do mtime não morde. Invariantes: **"sobreviveu" só sai do mutante compilado de verdade contra a suíte INTEIRA** — o subconjunto só decide morte, e quem o escolhe é a ferramenta; restaurar é **escrita nova**, nunca `os.utime` (mtime antigo deixa o binário do último mutante "em dia": medido, build de 1,9 s, nenhum `CoreCompile`); `killed` exige falha NOVA contra a rodada-base do MESMO escopo, e `inconclusive`/`build-error` nunca contam como morte; `proveBinary` compara a DLL final com a construída da árvore limpa. `dotnetBuildCommandFor` (exportado de `test-runner.js`) é a única grafia de "dotnet build" e `csharpClasses` a única leitura de classe de teste; `narrowNode` NÃO serve ao subconjunto porque recusa comando que já declara `--filter` (o `Category!=Eval` do GHLBrain), então o filtro é composto por `withFilter`. Dois parsers de TRX, ou um segundo "dotnet build" digitado, divergem |
153
154
  | `--record-baseline` (`recordTestBaselines` em `src/lib/tasks/test-nodes.js`) | `verify-runner.js` (`baselineTargetProblem` recusa ANTES do build; `recordBaselineFrom` só grava com todos os `runs[]` verdes), `src/commands/validation/verify.js` (não persiste stamp: é manutenção de config, não prova de gate), `framework/schemas/config.schema.json` (`expectedCount`) e `atomicWriteJson` de `state-manager.js` — o número é MEDIDO por comando, nunca digitado (D8). Com `{task}` é erro de uso ANTES do build (`baselineTargetProblem`, #87), e `recordTestBaselines` recusa de novo qualquer medição com `selection` — a segunda barreira mora no gravador |
154
155
  | `src/lib/tasks/eval-node.js` | os leitores do MESMO nó `runner: "evals"`: `test-nodes.js` (o enum de `TEST_RUNNERS` e a validação fail-closed do bloco `evals`), `verify-runner.js` (`preDecideRun` faz o pré-voo — os três estados de cache reprovam ANTES de `dotnet test`; `judgeEvalRun` lê o sumário depois), `verify.js` (`evalsStatusOf` deriva `nodes.evals` do stamp) e `framework/schemas/config.schema.json`. O pin tem UM leitor (`readAiPin`), chamado com duas raízes (`framework/` e `.morph/framework/`) — um segundo parser de `ai-pin.json` é como as duas verdades começam. **`null` é DESCONHECIDO, nunca 0**: sumário ilegível não pode fabricar queda contra `expectedCount`, e miss de cache é `fail` nomeado, JAMAIS `skip`. A unidade do nó é o CENÁRIO: só `judgeEvalRun` compara com `expectedCount` — `judgeRun` passa baseline `null` para `runner: "evals"` (#110 parte 2: os `[Fact]` do extrator contra a baseline de cenários reprovavam um 24/24), e sem sumário legível a contagem é `null`, nunca a do extrator — nem compara nem vira baseline. O sumário (`morph-eval-{id}-{pid}.json` no temp) é APAGADO antes e depois da rodada: um arquivo herdado sob o mesmo pid era lido como desta. `--linux` com nó de eval é erro de uso: o canal `MORPH_EVAL_*` carrega caminhos absolutos do host e o container roda uma cópia em `/work` |
155
156
  | `nodeStatuses()` em `src/commands/validation/verify.js` | os TRÊS blocos `nodes` de `framework/schemas/feature.schema.json`, todos `additionalProperties: false` — `verification.nodes`, `verifications.feature.nodes` e `taskVerifications.*.nodes`. A função é escritora ÚNICA dos três: um campo novo aqui sem os três no schema faz o histórico violar o próprio contrato. Canário: "os DOIS escritores do mapa de nós concordam" em `test/commands/verify.test.js` |
@@ -162,6 +163,13 @@ When modifying framework components, verify the full scope of related files.
162
163
  | `framework/hooks/shared/comment-rules.js` / `comment-scanner.js` / `source-files.js` (o hook `comment-guard`) | Os dois leitores do MESMO comentário: o hook `comment-guard` (PostToolUse `Write\|Edit`, bloqueia na hora da edição) e o `code-budget` (avisa no `verify`). Moram em `hooks/shared` porque hook não importa `src/`; o `src/` importa daqui, nunca uma cópia. O hook julga só o comentário ADICIONADO (`tool_response.originalFile`, medido: `null` no Write que cria) e só FORMA — processo, banner/`#region`, bloco > 3 linhas, doc-comment fora de pacote publicado —, nunca "repete o código". É o ÚLTIMO elo da chain `Write\|Edit` (um block encerra a chain; canário em `test/hooks/hooks-installer.test.js`). Marcador que template precisa manter (`READS STANDARD(S):`, `VERIFY:`) entra em `ALLOWED`; o canário de template em `test/hooks/comment-rules.test.js` reprova template que dispararia o hook no scaffold |
163
164
  | o ciclo de reuso (`reuse-map.md`) | `output-schema.js` (+ `npm run generate`), `framework/schemas/tasks.schema.json` + `tasks-json-parser.js` (campo `reuse`), `morph-plan` §1c/§5b, template de dispatch de `morph-implement`/`morph-apply`, personas de `agents.json`, `evals/rubrics/code-quality.md` + `condensed.md`, `morph-review` (insumo do avaliador), `test/framework/graph-fallback.test.js` |
164
165
  | `framework/mcp/registry.json` | mcp-installer.js, claude-config-detector.js, agents-installer.js (personas' `mcp` field), `scripts/validate-framework.js` Check 8, `test/framework/mcp-registry.test.js` |
166
+ | `src/lib/detectors/claude-config-detector.js` (`readEnabledPlugins`) e `src/lib/installers/mcp-installer.js` (`getInstalledMcps`) | os QUATRO chamadores de `orchestrateMcpSetup` (`mcp-setup.js` ×3, `init.js`): chamada sem `enabledPlugins` recria a duplicata `mcp__X` × `mcp__plugin_X_X` (canário de fiação em `test/lib/installers/mcp-installer.test.js`). `readEnabledPlugins` é a leitura ÚNICA de "plugin ligado" — o `doctor --procs` a reusa em `procs/mcp-config.js`; o prefixo `plugin_<nome>_` sai de `serverNames` do registry via `mcpServerNames` |
167
+ | `src/lib/procs/**` (`doctor --procs`) | `src/commands/project/doctor-procs.js`, o ramo `--procs` de `doctor.js` (entra DEPOIS do de `--worktrees`, fora das fatias que `test/commands/doctor-ai.test.js` corta), `bin/morph-spec.js` (`--prune` é UMA flag para os dois modos) e `test/commands/doctor-procs.test.js`. **Só `prune.js` mata, e por PID:** o canário varre o grafo estático do comando (`prune.js` só entra por import dinâmico dentro de `if (options.prune)`), reprova escrita, spawn e `process.kill` no caminho de leitura — com exceção nomeada ao `execFileSync` de `list.js` — e reprova `taskkill`/`Stop-Process`/`pkill`/`killall`/`/IM` em qualquer código de `src/` e dos hooks. As proteções de `classify.js` e a checagem de PID reaproveitado de `prune.js` têm teste por mutação sobre processo real: tirar uma delas mata um sleeper de teste. `null` é desconhecido, nunca lista vazia. `MORPH_PROCS_FIXTURE` é o único seam de teste; nada em produção o define |
168
+ | `src/lib/heavy/**` e o núcleo `framework/hooks/shared/heavy-{config,holders,slots,command}.js` (semáforo de nós pesados) + `process-alive.js` | `verify-runner.js` (`runVerify` é só o wrapper que cria UM `createHeavyGate` e o libera no `finally`; `gate.enter` é lazy e roda imediatamente antes do primeiro spawn real nos QUATRO pontos — build, testes, `--linux` e o e2e por `beforeHeavy` em `framework/hooks/shared/e2e-check.js`. Nó novo que spawna processo pesado sem `enter` fura o semáforo: `test/lib/verify-runner-heavy.test.js`), `verify.js`/`e2e.js` (`HeavyBusyError` → exit 3, **sem stamp e sem evento `Verify`**: fila não é gate; `nodeStatuses`/`PARTIAL_RUN_FLAGS` não ganham `busy`), `heavy.js` (`heavy status|run`), `cleanup.js` (`afterHeavyRun` recusa com `!enabled` — sem semáforo nada prova que outra sessão não compila —, fora de `feature|e2e-down|prune`, com holder vivo e com `dotnet build|test` vivo, e roda o `build-server shutdown` sob `runExclusive`), `doctor-procs.js` (holders são protegidos da poda; `afterHeavyRun` no `--prune`), `framework/schemas/config.schema.json` (bloco `heavy`; canário schema × `HEAVY_DEFAULTS` em `test/meta/heavy-isolation.test.js`) e `scripts/run-tests.mjs` (`HEAVY_OFF`: sem `MORPH_HEAVY=off` no env de todo filho a suíte enfileira nos slots REAIS de quem a roda). **O núcleo mora em `hooks/shared`** (hook não importa `src/`; `src/lib/heavy/` guarda só o `gate`, o `cleanup` e o comando): `heavy-holders.js` só lê (é o que o `doctor --procs` alcança no caminho somente leitura) e `heavy-slots.js` escreve; `graph-refresh.js` importa `pidAlive` em vez de ter cópia. **`heavy.gateBash` (opt-in):** `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`: um comando que um guard bloqueia não segura vaga) e `heavy-release` (`PostToolUse` e `PostToolUseFailure`, esta continua `async`) tomam e devolvem a vaga por `tool_use_id`. **A vaga de lease não tem pid** (`pid: null`, `leaseId`, `ttlMs`): `readLockFile` a vence por TTL, nunca por `pidAlive` — o processo do hook já saiu —, e uma chamada recusada por regra ou interrompida, que não dispara `Post*`, segura a vaga até o TTL. A espera do gate é `min(waitTimeoutSec, 540)`: um `PreToolUse` que estoura o timeout de 600 s deixa a chamada rodar, então a recusa tem de sair dali. **Nunca reescreva o comando para pôr slot:** o `updatedInput` é julgado contra as regras do usuário na forma reescrita. `heavyKind` (`heavy-command.js`) decide pelo executável de cada segmento fora de aspas; forma nova entra na tabela de `test/hooks/heavy-command.test.js` ao lado de um caso leve parecido. Os imports do semáforo em `heavy-gate`/`heavy-release` são **lazy** de propósito (os elos rodam em todo Bash e quase nenhum é pesado). Canários: `test/hooks/heavy-gate.test.js` (medido por mutação, 17 de 17), `heavy-slots`/`heavy-holders`/`heavy-command`, e os pins de ordem e `async` em `test/hooks/hooks-installer.test.js`. Slot vago = pid morto OU idade > `maxHoldMinutes`; `null`/diretório não criável é fail-open (não fabrica fila). `tearDownStack` (`e2e-check.js`) usa `--remove-orphans`: `test/lib/e2e/stack-teardown.test.js` pina o argv |
169
+ | `src/lib/worktree/agent-reaper.js` (reaper de sub-agent) + `framework/hooks/shared/agent-reap.js` | `worktree.js` (`runWorktreeRemove` é o núcleo sem `process.exit` que o reaper chama e `worktreeRemoveCommand` só imprime e sai; `teardownWorktree({ relinkOnFailure })` é opt-in do reaper — o `worktree remove` manual segue deixando a infra desmontada em falha, de propósito), `worktree-audit.js` (`agent-worktree-leftover`; a árvore `agent-<hex>` sai do ramo `morph-task` antigo para não duplicar o `unintegrated-work`), `doctor.js` (`--prune` roda o reaper), `worktree-context.js` (`kickAgentReap` no SessionStart: worker destacado, decisão só pelo disco, throttle de 10 min) e `bin/morph-spec.js`. **Provas — todas, senão preserva, e `null` preserva:** o último evento do `events.jsonl` da árvore é o `AgentUsage` do próprio `agentId`; graça; árvore limpa (`--untracked-files=all` filtrado por `withoutLeftoverIgnore`); zero commit exclusivo por `rev-list --stdin` **sem `--all`** e com os `morph-task/agent-*` fora do conjunto de refs; não é âncora de sessão. **O lock por repositório é obrigatório:** dois reapers concorrentes deixam um relink recriar junction no meio da remoção do outro, e o `--force` as atravessa. Canários: `test/lib/worktree/agent-reaper.test.js` (cada prova sozinha vira o veredito; medido por mutação), `test/commands/doctor-agent-reap.test.js`, `test/hooks/agent-reap.test.js`, `test/framework/worktree-reap-prose.test.js`. O `events.jsonl` da árvore removida vai para `finish-backup/task/` (`.morph/` é shieldable no `ignored-triage`) e some do `cost --all-trees` |
170
+ | `src/lib/worktree/vscode-settings.js` (`.vscode/settings.json`) | os SEIS pontos que o chamam: `init`/`update` (`update.js` pelo `fx`, e `setup-infra.js` step 18a — `init` delega ao `update`, a 2ª chamada sai `unchanged`), `runWorktreeSetup`/`runWorktreeProvision` (`rootAndEditorReport`, fail-open) e `doctor --worktrees --prune`. `planEditorSettings` só lê e `applyEditorSettings` escreve por `fx.writeFile`: o `update --dry-run` só é honesto porque o passo emite nos dois modos (canário de igualdade dry × real em `test/commands/update-dry-run.test.js`). **Só adiciona chave que falta**, nunca altera valor do usuário; comentário, vírgula sobrando, JSON ilegível, raiz não-objeto ou arquivo **rastreado** → não escreve e devolve o `snippet`. O arquivo **criado** vai para o `info/exclude` do common dir (`git-exclude.js`) — sem isso ele é sujeira da raiz e o `finish --merge` recusa. `bin/obj` em `files.exclude` só com stack .NET. `git.detectWorktrees` **não** é escrito de propósito (recurso de listagem, sobrepõe preferência global). `worktree.editorSettings` do `config.schema.json` e `MORPH_EDITOR_SETTINGS=0` desligam. Canários: `test/lib/worktree/vscode-settings.test.js` (medido por mutação, 11 de 11), `test/commands/editor-settings-wiring.test.js` |
171
+ | `framework/hooks/shared/worktree-root.js` (`worktree.root`) | o resolvedor ÚNICO de "onde as worktrees moram" e de "está no contêiner" — a UNIÃO do layout configurado com `worktrees/` e `.worktrees/`, para a árvore antiga seguir reconhecida. Todo leitor precisa dele, e um que volte ao literal fica cego para a externa ou acusa `out-of-container` em árvore boa: `worktree.js` (`buildWorktreePath`, mkdir do pai, `findWorktreeForFeature`), `worktree-audit.js` (`inContainer` e `dirsOnDisk`, que só varre `<root>/<repo>-<hash>/`), `agent-reaper.js`, `worktree-context.js` (`isOutsideContainer`), `worktree-create.js` (fallback em git puro), `registry.js` (`insideWorktreeContainer` lê o `.git` da árvore) e os scanners do dashboard (`features-scanner`, `project-api`). `src/lib/worktree/dirs.js` só reexporta as constantes daqui. **Root dentro da árvore é recusado com aviso:** as listas orientadas pelo nome `worktrees/` (`.gitignore`, `graphifyignore`, `test-runner`, `linux-runner`, `architecture-scanner`, `ai-project-scan`, globs do editor) não o enxergariam. A chave do repositório é `basename-<hash da raiz>`: só o basename colidiria entre repositórios. `buildWorktreePath` preserva a string como veio quando não há root (o teste dele passa `/project`). Canários: `test/hooks/worktree-root.test.js`, `test/commands/worktree-root.test.js` (medido por mutação, 14 de 14) |
172
+ | `src/lib/worktree/worktree-include.js` (`.worktreeinclude`) e o `locked` de `parseWorktreeList` | O hook `WorktreeCreate` **substitui** a criação nativa, e a doc do Claude Code diz que com hook o `.worktreeinclude` "não é processado": `copyWorktreeIncludes` é a paridade, chamado por `runWorktreeSetup` e `runWorktreeProvision` (que ainda copia `.env*` por `copyRootEnvFiles`; a união é de propósito, e o include não repete o que o `.env*` já trouxe). Casa o padrão **e** é ignorado NA ÁRVORE (`check-ignore` lá: arquivo que casa mas não é ignorado sujaria o worktree e o `finish` recusa), nunca `node_modules`/artefato de build (`isDisposableBuildArtifact`, de `ignored-triage.js`)/`.git` aninhado, nunca sobrescreve. Uma segunda noção de "o que copiar" faria o nativo e o morph discordarem. `parseWorktreeList` expõe `locked` (a linha `locked` do porcelain) e o `agent-reaper.js` o lê: `git worktree lock` é a marca que o Claude Code usa em agente rodando, e a árvore travada nunca é tocada (razão `locked`). O SessionStart segue o `cwd` do payload (`sessionDir` em `worktree-context.js`) só para worktree vinculado do MESMO repositório — `CLAUDE_PROJECT_DIR` fica na raiz numa sessão que entra no worktree —, e usa o `isSecondaryWorktree` de `git-paths.js` (a cópia local comparava `--git-dir` com `--git-common-dir` em formas diferentes e tratava uma SUBPASTA da raiz como worktree). Canários: `test/lib/worktree/worktree-include.test.js`, `test/hooks/worktree-context.test.js`, `test/lib/worktree/agent-reaper.test.js` |
165
173
  | Any `framework/` file that calls `morph-spec graph` | The fallback to grep must be declared in the SAME file — `test/framework/graph-fallback.test.js` enforces it (ADR-002: a skill that requires the graph with no fallback is a bug) |
166
174
  | `framework/hooks/claude-code/core/worktree-create.js` / `worktree-remove.js` / `block-raw-worktree-add.js` / `worktree-env-inject.js` | `hooks-installer.js` (`MORPH_HOOKS`: eventos `WorktreeCreate`/`WorktreeRemove` + a ORDEM da chain Bash — inject por último, um comando bloqueado nunca é reescrito), `hooks-scanner.js` `EVENT_KEYS`, `chain.js` (`updatedInput` só viaja com rewrite — `permissionDecision:'allow'` incondicional pularia o prompt de todo Bash), `worktree.js` (`runWorktreeProvision`, `--if-clean`, `envKeyForWorktree`), `worktree-audit.js` (`morph-task/*`, `out-of-container`), `worktree-context.js`, MORPH.md §2.2, `morph-apply` §3b-w, `test/framework/worktree-governance-prose.test.js`, `resident-size.test.js` (MORPH.md está NO teto). **Âncora de sessão:** `anchorSessionCommand` (`TEARDOWN_RE`) exporta `MORPH_SESSION_PROJECT_DIR`, que `sessionAnchoredInWorktree` (`worktree.js`) lê e que o `finish` (`--merge`/`--pr` adiam o desmonte) e o `worktree remove` (recusa `session-anchored-in-worktree`) consomem — mudar a chave ou o formato do comando num lado só devolve o bug de 48 hooks `MODULE_NOT_FOUND` depois do `finish` |
167
175
  | `src/lib/worktree/env-allocator.js` (`portLayout`) | os DOIS escritores do bloco — `buildEnvFileContent` (`.morph/worktree.env`) e `compose-env.js` (`buildComposeEnv`, usado por `e2e up`, pelo nó `e2e` do `verify`, por `stack-teardown.js` e por `morph-spec env`) — e os leitores `worktree-context.js`, `handoff-generator.js`, `worktree-env-inject.js`, o template `docker-compose.template.yml` (variáveis que ele publica) e o standard `local-compose-isolation.md` (tabela de offsets). Canário: "worktree.env e compose env concordam byte a byte" em `test/lib/e2e/compose-env.test.js`. Offset novo aqui sem entrar no template é porta que ninguém publica |
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 8.42.0
6
+ **Version:** 8.43.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -379,4 +379,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
379
379
 
380
380
  ---
381
381
 
382
- *morph-spec v8.42.0 by [Polymorphism Tech](https://polymorphism.tech)*
382
+ *morph-spec v8.43.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -30,11 +30,14 @@ const installPluginCommand = lazy('../src/commands/project/install-plugin-cmd.js
30
30
  const updateCommand = lazy('../src/commands/project/update.js', 'updateCommand');
31
31
  const doctorCommand = lazy('../src/commands/project/doctor.js', 'doctorCommand');
32
32
  const statusCommand = lazy('../src/commands/project/status.js', 'statusCommand');
33
+ const heavyStatusCommand = lazy('../src/commands/project/heavy.js', 'heavyStatusCommand');
34
+ const heavyRunCommand = lazy('../src/commands/project/heavy.js', 'heavyRunCommand');
33
35
  const worktreeSetupCommand = lazy('../src/commands/project/worktree.js', 'worktreeSetupCommand');
34
36
  const worktreeProvisionCommand = lazy('../src/commands/project/worktree.js', 'worktreeProvisionCommand');
35
37
  const worktreeLinkCommand = lazy('../src/commands/project/worktree.js', 'worktreeLinkCommand');
36
38
  const worktreeListCommand = lazy('../src/commands/project/worktree.js', 'worktreeListCommand');
37
39
  const worktreeRemoveCommand = lazy('../src/commands/project/worktree.js', 'worktreeRemoveCommand');
40
+ const worktreeReapCommand = lazy('../src/commands/project/worktree-reap.js', 'worktreeReapCommand');
38
41
  const branchPruneCommand = lazy('../src/commands/project/branch.js', 'branchPruneCommand');
39
42
  const fleetCommand = lazy('../src/commands/project/fleet.js', 'fleetCommand');
40
43
  const envCommand = lazy('../src/commands/project/env.js', 'envCommand');
@@ -73,6 +76,7 @@ const graphExplainCommand = lazy('../src/commands/graph/explain.js', 'graphExpla
73
76
  const validateCommand = lazy('./validate.js', 'validateCommand');
74
77
  const validateFeatureCommand = lazy('../src/commands/validation/validate-feature.js', 'validateFeatureCommand');
75
78
  const verifyCommand = lazy('../src/commands/validation/verify.js', 'verifyCommand');
79
+ const mutateCommand = lazy('../src/commands/validation/mutate.js', 'mutateCommand');
76
80
  const e2eUpCommand = lazy('../src/commands/e2e/e2e.js', 'e2eUpCommand');
77
81
  const e2eStatusCommand = lazy('../src/commands/e2e/e2e.js', 'e2eStatusCommand');
78
82
  const e2eDownCommand = lazy('../src/commands/e2e/e2e.js', 'e2eDownCommand');
@@ -154,8 +158,9 @@ program
154
158
  .option('--mcp', 'Check MCP server configuration')
155
159
  .option('--ai', 'Auditar um projeto .NET com agentes: pin × projeto, API renomeada, tarifa e `api` por alias, prompt sem fonte')
156
160
  .option('--worktrees', "Reconcile git's worktree registry × the directories on disk × known features")
157
- .option('--prune', "With --worktrees: drop git records for worktree directories that are already gone, clear any feature's root worktree registration pointing at one of them, and apply the finish-backup retention (.git/morph-spec/finish-backup) — the only path that also prunes backups written before it (no manifest)")
158
- .option('--json', 'With --worktrees: output as JSON')
161
+ .option('--procs', 'Listar node/dotnet/testhost/esbuild/chrome headless sem pai vivo, MCP duplicado (projeto × plugin) e .wslconfig ausente. Somente leitura; os consertos de MCP e WSL são só sugeridos')
162
+ .option('--prune', "With --worktrees: drop git records for worktree directories that are already gone, clear any feature's root worktree registration pointing at one of them, and apply the finish-backup retention (.git/morph-spec/finish-backup) — the only path that also prunes backups written before it (no manifest). With --procs: encerrar por PID só os órfãos sem proteção (nunca por nome de imagem, nunca MCP configurado nem worker de build)")
163
+ .option('--json', 'With --worktrees or --procs: output as JSON')
159
164
  .option('--reset', 'Remove morph-managed entries from .claude/settings.local.json')
160
165
  .action(doctorCommand);
161
166
 
@@ -415,6 +420,19 @@ program
415
420
  verifyCommand(feature, task, { ...options, noPersist: options.persist === false })
416
421
  );
417
422
 
423
+ program
424
+ .command('mutate [feature]')
425
+ .description('Prova de mutação do Gate 3 em lote: lê o manifesto de mutantes, roda todos (um build de schemata em .NET, subconjunto primeiro, suíte inteira só para sobrevivente), restaura os bytes e imprime a seção `## Mutações`')
426
+ .option('--plan <file>', 'Manifesto JSON (default: .morph/features/<feature>/4-review/mutants.json)')
427
+ .option('--out <file>', 'Onde gravar a seção `## Mutações` (default: 4-review/mutation-section.md)')
428
+ .option('--no-subset', 'Sem subconjunto: cada mutante roda a suíte inteira')
429
+ .option('--allow-dirty', 'Permite mutar arquivo com alteração não commitada (o snapshot é o único caminho de volta)')
430
+ .option('--jobs <n>', 'Rodadas de subconjunto em paralelo (default 2); cada worker toma um slot heavy livre, e sem slot livre a rodada degrada para sequencial', (v) => Number.parseInt(v, 10))
431
+ .option('--timeout <ms>', 'Teto de cada comando de build/teste', (v) => Number.parseInt(v, 10))
432
+ .option('--restore', 'Recupera o disco de uma rodada que morreu no meio: restaura os bytes do journal, sem rodar nada')
433
+ .option('--json', 'Output as JSON')
434
+ .action((feature, options) => mutateCommand(feature, options));
435
+
418
436
  // ─── Scaffolding ──────────────────────────────────────────────────────────
419
437
  const scaffoldCmd = program
420
438
  .command('scaffold')
@@ -487,6 +505,23 @@ e2eCommand
487
505
  .option('--json', 'Output as JSON')
488
506
  .action((feature, options) => e2eDownCommand(feature, options));
489
507
 
508
+ // ─── Heavy nodes (build, tests, e2e) ─────────────────────────────────────
509
+ const heavyCommand = program
510
+ .command('heavy')
511
+ .description('Slots por usuário para os nós pesados (build, testes, e2e), compartilhados entre as sessões');
512
+
513
+ heavyCommand
514
+ .command('status')
515
+ .description('Quem segura os slots, a RAM livre e o diretório do semáforo (somente leitura)')
516
+ .option('--json', 'Output as JSON')
517
+ .action((options) => heavyStatusCommand(options));
518
+
519
+ heavyCommand
520
+ .command('run <command...>')
521
+ .description('Rodar um comando pesado dentro de um slot: morph-spec heavy run [--label x] -- dotnet build. Repassa o exit code; exit 3 = ocupado, não é vermelho')
522
+ .option('--label <label>', 'Nome do que roda, mostrado a quem espera pelo slot')
523
+ .action((command, options) => heavyRunCommand(command, options));
524
+
490
525
  // ─── Worktree (optional, isolation) ───────────────────────────────────────
491
526
  const worktreeCommand = program
492
527
  .command('worktree')
@@ -531,6 +566,17 @@ worktreeCommand
531
566
  .option('--json', 'Output as JSON (always JSON)')
532
567
  .action((feature, options) => worktreeRemoveCommand(feature, options));
533
568
 
569
+ // Sub-agent trees (`agent-<hex>` on `morph-task/agent-<hex>`) outlive the agent: nothing
570
+ // tells the harness the agent is done with its worktree. The reaper removes only what it can prove
571
+ // is finished and lost-proof; the rest is listed with the reason.
572
+ worktreeCommand
573
+ .command('reap')
574
+ .description('Remove the worktrees and branches of sub-agents that stopped clean (nothing uncommitted, no commit only they hold); everything else is listed with the reason and left alone')
575
+ .option('--dry-run', 'Report what would be removed without touching anything')
576
+ .option('--grace <minutes>', 'Minutes a stopped agent must stay quiet before its tree is reaped (default 30, or worktree.reap.graceMinutes)')
577
+ .option('--json', 'Output as a single JSON line')
578
+ .action((options) => worktreeReapCommand(options));
579
+
534
580
  worktreeCommand
535
581
  .command('list')
536
582
  .description('List active feature worktrees with their phase and gate status')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.42.0",
3
+ "version": "8.43.0",
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",
@@ -187,4 +187,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
187
187
 
188
188
  ---
189
189
 
190
- *morph-spec v8.42.0 by Polymorphism Tech*
190
+ *morph-spec v8.43.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.42.0 by Polymorphism Tech*
252
+ *morph-spec v8.43.0 by Polymorphism Tech*
@@ -61,7 +61,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
61
61
  | `retype <f> <tipo>` | Muda o workType mid-flight; bloqueado após o Gate 3 |
62
62
  | `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 |
63
63
  | `archive <f>` | Arquiva sem integrar (legadas/faxina) |
64
- | `worktree setup\|provision\|link\|remove\|list` | Ciclo do worktree; `provision` = isolamento de task; `remove` é a única remoção segura |
64
+ | `worktree setup\|provision\|link\|remove\|list\|reap` | Ciclo do worktree; `provision` = isolamento de task; `remove` é a única remoção segura |
65
65
  | `branch prune [--dry-run]` | Apaga branch já integrada, inclusive squash-merge; nunca a com PR aberto |
66
66
  | `handoff <f>` | Regenera o documento de retomada da feature |
67
67
  | `fleet` | Visão agregada de toda feature ativa (raiz + worktrees) |
@@ -83,14 +83,15 @@ Prefixo de todo comando: `morph-spec`. Flags/exemplos: `.morph/framework/CLI.md`
83
83
  | Skill | Quando usar |
84
84
  |-------|-------------|
85
85
  | `morph-brainstorming` | Entender negócio, escanear stack, produzir proposal.md |
86
- | `morph-stack-scan` | Detectar desvios de stack em projeto brownfield |
87
- | `morph-uiux` | Ramo opcional uiux — design system, mockups, componentes, fluxos |
86
+ | `morph-stack-scan` | Desvios de stack em projeto brownfield |
87
+ | `morph-uiux` | Ramo opcional de UI/UX — design system e mockups |
88
88
  | `morph-plan` | Destilar use cases e produzir spec.md + mandate.md + tasks.json |
89
89
  | `morph-implement` | Executar uma tarefa com auto-advance entre tasks |
90
90
  | `morph-eval` | Aplicar rubricas e produzir score 0-10 |
91
91
  | `morph-review` | Produzir review-report.md (Gate 3) |
92
- | `morph-hotfix` | ADW cirúrgico de hotfix — scout → fix → gate humano → ship |
92
+ | `morph-hotfix` | Hotfix cirúrgico (`/morph-hotfix`) |
93
93
  | `morph-standards` | Localizar standard sob demanda |
94
+ | `morph-machine` | RAM/CPU: fila pesada, órfãos, worktrees sobrando |
94
95
 
95
96
  ## Roteamento por tipo de trabalho
96
97
 
@@ -118,14 +118,14 @@ 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
- - **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.
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 no contêiner (`worktrees/` ou `worktree.root`); fora dele = criado sem o morph (`doctor --worktrees`). Já existe: abra o `claude` DENTRO dela; `EnterWorktree(path)` sempre pede aprovação. 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. 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. Sobra de sub-agent (`agent-<hex>`): `worktree reap` remove só a que parou limpa e sem commit exclusivo.
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). Migration destrutiva → integre antes de abrir irmãos. Build/testes/e2e disputam 2 slots por usuário (`heavy status`): **exit 3 = fila cheia, não vermelho** — repita; fora do `verify`, `heavy run -- <cmd>`.
124
124
  - **Limitação:** `node_modules` é junction compartilhada — dependência mudada afeta os irmãos; integre antes de seguir em paralelo.
125
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.
126
- - **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira); `worktree list` é a visão enxuta por worktree.
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.
128
- - **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único; `--merge` integra na hora, **`--pr` não** — só abre o PR. **Arquiva** commitando `features/{f} → archive/{f}` **dentro de `morph/{f}`**: `--merge` faz `--no-ff` **local** (sem push; recusa se sujo; aborta em conflito); `--pr` faz `push` + `gh pr create` (exige `origin`+`gh`, checados **antes** de arquivar) — fica `aguardando-merge` até o merge, **nunca "entregue" só por `finished: true`**. Worktree desmontado (PROVE-or-DECLINE), portas liberadas; no `--pr` a branch é **preservada**. `morph-spec archive` fica para arquivar-sem-integrar (features legadas, faxina).
126
+ - **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira).
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).
128
+ - **Fechamento:** aprove o Gate 3, depois **`morph-spec finish <f> --pr|--merge`** — encerramento único; `--merge` integra na hora, **`--pr` não** — só abre o PR. **Arquiva** commitando `features/{f} → archive/{f}` **dentro de `morph/{f}`**: `--merge` faz `--no-ff` **local** (sem push; recusa se sujo; aborta em conflito); `--pr` faz `push` + `gh pr create` (exige `origin`+`gh`, checados **antes** de arquivar) — fica `aguardando-merge` até o merge, **nunca "entregue" só por `finished: true`**. Worktree desmontado (PROVE-or-DECLINE), portas liberadas; no `--pr` a branch é **preservada**.
129
129
 
130
130
  ---
131
131
 
@@ -353,11 +353,12 @@ Path de cada skill: `.claude/skills/{nome}/SKILL.md`.
353
353
  | morph-review | Invocada pela morph-apply — produz review-report + Gate 3 |
354
354
  | morph-hotfix | **Orquestrador**: ADW cirúrgico de hotfix (scout → fix → gate humano → ship) |
355
355
  | morph-standards | Quando precisar localizar ou aplicar um standard específico |
356
- | morph-media | Produzir imagem/vídeo por IA — modelo, prompt versionado, custo, gate humano entre candidatas |
356
+ | morph-media | Imagem/vídeo por IA — modelo, prompt versionado, custo, gate humano |
357
+ | morph-machine | Máquina lenta/sem RAM, "⏳ não rodou" ou exit 3, sobra de worktree de sub-agent, sub-agents pesados em paralelo |
357
358
 
358
- Slash commands (`/morph-proposal`, `/morph-apply`, `/morph-hotfix` — linhas acima) são atalhos que invocam a `Skill()` correspondente; o agente pode chamá-la direto quando o usuário pede "use /morph-proposal".
359
+ Slash commands (`/morph-proposal`, `/morph-apply`, `/morph-hotfix`) são atalhos da `Skill()` correspondente; chame-a direto quando o usuário pedir "use /morph-proposal".
359
360
 
360
- Cada skill contém instruções passo a passo — leia, siga, não desvie sem justificativa no recap.md. Só as skills **core** do framework são instaladas. Nenhuma skill é invocada proativamente por hook ou script — você decide quando a situação pede o rigor dela.
361
+ Cada skill traz instruções passo a passo: siga, e justifique desvio no recap.md. Só as skills **core** são instaladas; nenhum hook as invoca — você decide quando a situação pede o rigor dela.
361
362
 
362
363
  ---
363
364
 
@@ -357,7 +357,7 @@
357
357
  ],
358
358
  "model": "opus",
359
359
  "maxTurns": 90,
360
- "persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Você também tem Playwright: quando o nó `e2e` do verify subiu o stack, NAVEGUE a aplicação como um usuário faria e percorra os fluxos da feature — ler o diff prova que o código existe, usar a aplicação prova que ela funciona. Confira console e network atrás de erro que nenhum teste cobre. Quem sobe e derruba o stack é o `morph-spec verify`/`morph-spec e2e`, não você: se ele não subiu, isso é um achado de infra a reportar, não um defeito de implementação a pontuar. Se um fluxo que você exercitou merece virar regressão, recomende a spec Playwright no relatório — recomendar é seu papel, escrever não. Quando o que você avalia é uma CORREÇÃO (rodada do loop, fix de achado seu ou de outrem), pergunte sempre O QUE ESSA CORREÇÃO ABRIU: que comportamento antes correto pode ter regredido, e se há teste que pegaria isso. Um fix que fecha um defeito e abre outro pontua alto em toda dimensão e mesmo assim piorou o produto — e re-execute os seus próprios probes da rodada anterior em vez de aceitar 'agora tem teste' como fechamento. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto. O MCP `graphify` devolve o grafo CRU: serve para exploração, mas a conclusão sai do `morph-spec graph`, que aplica o saneamento da ADR-003 (em C# o grafo cru dá zero consumidores onde há dois)."
360
+ "persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Você também tem Playwright: quando o nó `e2e` do verify subiu o stack, NAVEGUE a aplicação como um usuário faria e percorra os fluxos da feature — ler o diff prova que o código existe, usar a aplicação prova que ela funciona. Confira console e network atrás de erro que nenhum teste cobre. Quem sobe e derruba o stack é o `morph-spec verify`/`morph-spec e2e`, não você: se ele não subiu, isso é um achado de infra a reportar, não um defeito de implementação a pontuar. Se um fluxo que você exercitou merece virar regressão, recomende a spec Playwright no relatório — recomendar é seu papel, escrever não. Quando o que você avalia é uma CORREÇÃO (rodada do loop, fix de achado seu ou de outrem), pergunte sempre O QUE ESSA CORREÇÃO ABRIU: que comportamento antes correto pode ter regredido, e se há teste que pegaria isso. Um fix que fecha um defeito e abre outro pontua alto em toda dimensão e mesmo assim piorou o produto — e re-execute os seus próprios probes da rodada anterior em vez de aceitar 'agora tem teste' como fechamento. PROVA DE MUTAÇÃO: quem a executa é `morph-spec mutate` (o método e a ordem estão no prompt de dispatch da `morph-review`) — você escolhe os mutantes no call site que carrega a decisão, uma rodada por cláusula, e escreve o manifesto (barra invertida do código como `{{BS}}`, que a ferramenta expande); o subconjunto de testes é da ferramenta, só a suíte inteira prova sobrevivência, e nunca reverta por `git checkout --` nem devolvendo o mtime com `os.utime`. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto. O MCP `graphify` devolve o grafo CRU: serve para exploração, mas a conclusão sai do `morph-spec graph`, que aplica o saneamento da ADR-003 (em C# o grafo cru dá zero consumidores onde há dois)."
361
361
  },
362
362
  "scout": {
363
363
  "title": "Codebase Scout",
@@ -32,6 +32,10 @@
32
32
  **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).
33
33
  - **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
34
34
  - **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
35
+ - **`morph-spec worktree reap [--dry-run] [--grace <min>] [--json]`** — remove as **sobras de sub-agent**: o worktree `agent-<hex>` (branch `morph-task/agent-<hex>`, o que o `Agent` com `isolation: "worktree"` deixa para trás) e a branch órfã `morph-task/agent-<hex>`. **Só remove se TODAS as provas fecham; na dúvida preserva e lista com a razão:** (1) é de agent — diretório `agent-<hex>` sob `worktrees/` e branch `morph-task/agent-<hex>` (ou HEAD destacado); `morph-task/<nome>`, features e `claude -w` nunca entram; (2) o agente **parou** — o último evento do `.morph/logs/events.jsonl` da árvore é o `AgentUsage` dele (`agentId` = o `<hex>`); sem esse evento só lista (`no-usage-event`); (3) **graça** — o log parou de crescer há ≥ `worktree.reap.graceMinutes` (30; `--grace`), porque um agente parado ainda pode ser retomado por `SendMessage`; (4) **árvore limpa** — `git status --porcelain --untracked-files=all`; arquivo não rastreado também bloqueia (`dirty`); (5) **sem commit exclusivo** — `git rev-list <tip> --not <refs>`, com `refs` = branches locais, remotas e tags **menos todas as `morph-task/agent-*`**. Nunca `--all`: ele conta o HEAD de cada worktree e o da própria árvore protegeria o commit; e dois agentes no mesmo commit exclusivo se protegeriam um ao outro — assim preservam os dois; (6) não é o cwd nem a âncora da sessão. Medir não deu (`null`) sempre preserva. **Como remove:** pelo núcleo do `worktree remove --target --if-clean` (desmonta as junctions antes, backup do que só existe ali em `finish-backup/task/`, retenção por namespace), nunca `rm -rf`; **falha de remoção devolve as junctions** (o `worktree remove` manual as deixa desmontadas de propósito): o agente pode estar vivo, e sem `.claude` os hooks dele morrem. Depois apaga a branch (`git branch -D`, com a contagem de commits exclusivos refeita antes). **Um lock por repositório** (`<git-common-dir>/morph-spec/agent-reap.lock`) serializa dois reapers: sem ele, o relink de um recriaria junctions no meio da remoção do outro, e o `--force` as atravessaria; lock de pid vivo → `busy: true` e nada é feito. Branch órfã sai pela mesma regra 5, com a graça medida pelo reflog da branch (o `provision` cria a branch antes do worktree). Idempotente: a 2ª rodada não acha nada. **Quem dispara:** as skills (`morph-apply` §3b-w passo 4, `morph-implement` pós-dispatch, `morph-review` depois de ler o `evaluator-report`), o SessionStart (`worktree-context` solta um worker destacado, só na raiz, no máximo 1 a cada 10 min e só se houver `agent-<hex>` no disco; `MORPH_WORKTREE_REAP=0` desliga) e o `doctor --worktrees --prune`. `--json`: `{ ok, dryRun, graceMinutes, reaped[], branchesDeleted[], wouldReap[] (só no dry-run), preserved[{path,branch,hex,reason,detail}], active, busy? }`. **Telemetria:** remover a árvore leva o `events.jsonl` dela (o custo do agent em `cost --all-trees`); o conteúdo de `.morph/` fica no backup `finish-backup/task/`, como já ficava no `worktree remove --target` do `morph-apply`.
36
+ - **`.vscode/settings.json` (editor)** — `init`, `update`, `worktree setup|provision` e `doctor --worktrees --prune` adicionam a `files.watcherExclude`, `search.exclude` e `files.exclude` os globs `**/worktrees/**` (e o legado `**/.worktrees/**`) e `**/node_modules/**`; `**/bin/**` e `**/obj/**` entram em `watcherExclude` e `search.exclude` sempre e em `files.exclude` só em projeto .NET (stack `dotnet` na config, ou `.sln`/`.slnx`/`.csproj` na raiz), porque num projeto JS o `bin/` guarda código-fonte. **Só adiciona o que falta:** chave do usuário, ordem e valor existente ficam (`"**/bin/**": false` continua `false`), e a 2ª rodada não muda um byte. **JSONC:** não há escritor que preserve comentário, então arquivo **com comentário, com vírgula sobrando, ilegível, com raiz que não é objeto, ou rastreado pelo git** não é escrito — o relatório (`editorSettings` no `--json` de `setup`/`provision`, `prune.editorSettings` no do `doctor`) traz `status: "suggest"`, a `reason` e o `snippet` só com o que falta, para colar. Legível e sem comentário, reescreve preservando BOM, EOL e indentação. **O arquivo criado** entra no `info/exclude` do common dir (`git-exclude.js`): não rastreado, contaria como sujeira da raiz, e o `finish --merge` recusa raiz suja. Desliga: `worktree.editorSettings: false` na config, ou `MORPH_EDITOR_SETTINGS=0`. O `update --dry-run` prevê a escrita (`writeFile`). **O que NÃO cobre:** as chaves cortam watcher, busca e explorer — não governam a descoberta do Git nem o language server. `git.repositoryScanMaxDepth` (padrão 1) não alcança `worktrees/<n>` (profundidade 2), e `git.detectWorktrees` vem **desligado** por padrão (`false`, `scope: resource`; lido no `package.json` da extensão Git do VS Code 1.124.2) e é um recurso de LISTAGEM: mostra as worktrees em *Source Control Repositories*, até `git.detectWorktreesLimit` (50). Ligá-lo faz o VS Code enxergar mais repositórios, não menos, e a escrita no workspace sobrepõe uma preferência global do usuário — por isso o morph-spec não o grava; quem o quer liga na própria config. Não medido: se o C# Dev Kit respeita `files.exclude`, e o antes/depois de RAM e de processos do VS Code com as exclusões.
37
+ - **`worktree.root` (worktrees fora da árvore aberta)** — chave de `.morph/config/config.json` (ou `MORPH_WORKTREE_ROOT`, por rodada): absoluta, `~/…` ou relativa à raiz primária. As worktrees **novas** (`setup`, `provision`, o hook `WorktreeCreate` e o fallback dele em git puro) nascem em `<root>/<repo>-<hash>/<nome>`; o hash é da raiz do repositório, para dois repositórios de mesmo nome dividirem o mesmo root sem colidir. **Sem a chave, nada muda.** **Só aceita root FORA da árvore:** um valor que resolve para dentro da raiz do projeto é **ignorado com aviso** (`worktreeRootWarning` no `--json` de `setup`/`provision`) e valem `worktrees/`, porque toda lista orientada por esse nome (`.gitignore`, `graphifyignore`, descoberta de testes, o `tar` do `--linux`, os globs do editor) deixaria de enxergá-lo em silêncio. Um root **acima** da raiz (`..`) é aceito: o contêiner vira um irmão do repositório. **"Está no contêiner" é a UNIÃO** do layout configurado com `worktrees/` e `.worktrees/`: a árvore que já existe continua reconhecida depois que a chave é ligada ou trocada, e uma criada em qualquer outro lugar segue acusada como `out-of-container`. O resolvedor é um só (`framework/hooks/shared/worktree-root.js`; hook não importa `src/`) e é lido por `buildWorktreePath`, pelo mkdir do pai e por `findWorktreeForFeature`; pelo `doctor --worktrees` (`inContainer` e a varredura de resíduo, que olha só `<root>/<repo>-<hash>/` — nunca a pasta de outro repositório, senão o `--prune` apagaria pastas alheias); pelo reaper; pelo SessionStart (`isOutsideContainer`); pelo registro de projetos (a worktree é detectada pelo `.git` ser um arquivo que aponta para `.git/worktrees/`, não pelo nome do diretório pai); e pelos scanners do dashboard. `finish`, `remove` e `list` andam pelo `git worktree list` e não dependem do lugar. **Não faz:** trocar a chave não move as worktrees que já existem.
38
+ - **Modelo de trabalho: o Claude Code é a porta de entrada, o morph é o dono da worktree** — `claude -w <nome>`, o `EnterWorktree` (por nome) e o `isolation: "worktree"` de sub-agent passam pelo hook `WorktreeCreate` do morph. Segundo a doc do Claude Code, o hook **substitui por inteiro** a criação nativa (o `git worktree add`, o caminho e a branch) e pode devolver **qualquer** caminho na 1ª linha do stdout (o payload traz `name`, `worktree_path` sugerido, `branch_name` e `source_repo_path`; o morph lê `name`). Por isso entram como `worktree setup <f>` (nome de feature conhecido) ou `worktree provision <n>` (o resto), já com a infra, o bloco de portas e o `morph/…`/`morph-task/…`. **O que a criação nativa daria e o morph faz por conta própria, porque o hook a substitui:** `worktree.baseRef` (o hook lê as três camadas de settings), `.worktreeinclude` (a doc: com hook ele "não é processado"; `copyWorktreeIncludes` o honra — o que casa o padrão **e** é ignorado na árvore, nunca `node_modules`/artefato de build/`.git` aninhado, nunca sobrescreve, idempotente; o `provision` continua copiando `.env*` e o resultado sai em `worktreeInclude` no `--json`; **custo medido:** 0,15 s numa raiz com 6 mil arquivos em `node_modules` e 0,5 s (1,4 s a frio) no MORPH_GHLBrain, contra o timeout de 120 s do `WorktreeCreate`, e um pathspec de exclusão não poda a caminhada; **nunca `--exclude-standard`**, que com `--ignored` devolve todo o ignorado — 57 560 caminhos no GHLBrain em vez dos 4 que casam) e a cópia de `.env*`. **Abrir uma worktree que já existe:** abra o `claude` DENTRO dela — uma feature = um worktree = uma instância. `EnterWorktree(path)` para um caminho fora de `.claude/worktrees/` pede aprovação **a cada vez** (regra de permissão e "não perguntar de novo" não a suprimem; só `bypassPermissions`), e uma sessão que entra no meio mantém `CLAUDE_PROJECT_DIR` na raiz e move só o `cwd` do payload: por isso o SessionStart do morph (`worktree-context.js`, `sessionDir`) segue o `cwd` quando ele é um worktree vinculado do MESMO repositório, e a identidade, o bloco de portas e o handoff chegam. **Limpeza:** o sweep periódico nativo mantém qualquer worktree sem o marcador que o Claude grava ("including a worktree a WorktreeCreate hook created"), então **nunca** limpa as do morph; quem limpa é o `worktree reap`, que respeita `git worktree lock` (a marca que o Claude usa em agente rodando) e devolve `locked` como razão. O `WorktreeRemove` (fim de sub-agent e saída de sessão) chama `worktree remove --if-clean`; ele não dispara em `-p` (medido), e é esse o resíduo que o reaper pega. **Isolamento nativo:** numa sessão isolada por `--worktree`/`EnterWorktree` o Claude bloqueia edição, `cwd` de comando e redirect de git (`git -C`, `GIT_DIR`, `cd` para a raiz) para o checkout principal — `finish`, `worktree remove --target` e `worktree reap` rodam da raiz, não de dentro. A remoção nativa não atravessa mais junction no Windows (v2.1.205), mas o `git worktree remove` cru continua atravessando: a regra "só o morph remove" segue valendo. **Não medido:** se o SessionStart de `claude -w` já chega com o `cwd` no worktree, e o `claude -w "#1234"` (PR): o hook recebe um `name` e cai no `--base` do morph, não no head do PR. Fontes: `code.claude.com/docs/en/worktrees` e `/hooks`.
35
39
  - **Backups do finish (`.git/morph-spec/finish-backup/`) e a retenção** — o que o `finish` (o shed da raiz, `{f}-{carimbo}/`) e o desmonte do worktree (`{f}-{carimbo}-worktree-ignored/`) copiam antes de apagar um arquivo que não existe em outro lugar; `worktree remove` de árvore de task grava em `task/{n}-{carimbo}-worktree-ignored/`. Cada pasta ganha um manifesto ao lado (`{pasta}.manifest.json`): o que foi copiado, os caches deixados de fora (`excludedCaches`) e os **nomes** dos `.env*` que entraram (`secrets`). **Um finish é UM backup**: as duas pastas dele levam o mesmo carimbo e contam juntas; pastas gravadas antes disso têm carimbos diferentes e contam uma a uma. **Retenção ligada por padrão**, também em projeto cujo `config.json` não tem a chave: os **20 backups mais novos de cada namespace** (a raiz de `finish-backup/` e `task/` têm orçamentos separados — um fan-out de sub-agents não despeja o backup da feature fechada um minuto antes) **sempre ficam, de qualquer idade** — não há corte por idade no padrão, então um período parado nunca zera os backups das últimas features fechadas —; dentro de um backup que fica, os arquivos `.env*` são apagados depois de **7 dias** (o resto fica, e o manifesto guarda os nomes e `secretsStrippedAt`). Configurável em `config.json → finishBackup.retention: { maxCount, maxAgeDays, secretMaxAgeDays }` (inteiros ≥ 1; `maxAgeDays` é um corte por idade OPCIONAL — ausente ou `null`, o padrão, não corta) ou desligada com `finishBackup.retention: false` (schema em `framework/schemas/config.schema.json`, autoridade em `resolveBackupRetention()`). **O que não dá para ler PULA a poda naquela execução, nunca cai no padrão** — é uma operação que apaga, e um `{ "enabled": false }`, um `"false"`, um `maxCount: 0`, uma chave desconhecida, um `finishBackup` que não é objeto ou um `config.json` que não parseia podem muito bem querer dizer "guarde tudo": a saída diz `backupRetention: {skipped: "<razão>"}`, o `doctor --worktrees --prune` avisa com a razão, e o `doctor`/`doctor --worktrees` acusam até alguém corrigir. Só a ausência da chave liga o padrão. **Backup sem manifesto é legado** (gravado antes desta retenção) — esse é o critério, e nenhum outro. A poda automática nunca o apaga, nunca tira os `.env*` dele e não lhe dá vaga no orçamento; ela só conta quantos esperam (`backupRetention.legacyPending: N` + `legacyHint`). **A primeira poda deles é só com `doctor --worktrees --prune` explícito**, que os ranqueia junto com os demais (contando no teto), apaga os que ficam fora e grava um manifesto (`legacy: true`, `adoptedAt`) em cada um que mantém — dali em diante são backups comuns. A poda automática roda **depois** do backup novo — no fim de todo `finish` com `finished: true` e de todo `worktree remove` que gravou backup (a saída traz `backupRetention: {removed, removedSample, secretsStripped, declined, legacyPending, legacyHint}` só com o que se aplica). **O backup que a execução acabou de gravar nunca é podado por ela** — o carimbo dela é protegido —, e backup com carimbo no FUTURO (relógio adiantado e depois corrigido) fica e não ocupa vaga na contagem. O `finish --dry-run` anuncia só o que a poda AUTOMÁTICA faria (`backupRetention: {outsideRetention, overdueSecrets, legacyPending}` + uma linha em `wouldDo`), sem apagar nada. Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o resto do desmonte: backup com reparse point dentro é mantido (`declined`) — mas perde os `.env*`, porque a política o queria fora —, link nunca é atravessado (nem um `task/` que seja junction), e **só `task/` é namespace**: qualquer outra pasta de primeiro nível que não tenha nome de backup nunca é visitada nem removida, nem vazia. O `.env*` idêntico ao da raiz nunca chega a ser copiado (é replicável, ver acima); o que chega é o que difere — o `.env.e2e` de cada worktree carrega o bloco de portas dela, então nenhum é igual ao da raiz, e é por isso que o prazo curto existe.
36
40
  - **`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.
37
41
  - **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
@@ -41,22 +45,25 @@
41
45
  ## Validação
42
46
 
43
47
  - **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. UC com nome fora do padrão `UC-{nn}-{slug}.md` (slug com maiúscula ou `_`) continua encontrado, com aviso: o `finish` não o promove ao acervo. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. Os achados de ciclo de vida do `usecase check` (task sobre UC `superseded`, revogação que o `finish` não aplicaria) saem sob `⚠ Ciclo de vida do acervo`, também só aviso, na chave `lifecycle`. **Tamanho de task:** task com mais de 8 `outputs` (`MAX_TASK_OUTPUTS`) é **erro** (exit 1) enquanto o Gate 2 não foi aprovado — fatie por pós-condição — e só aviso depois dele (chore/hotfix nascem com o plano aprovado por política); o `--json` traz `taskSize: {max, oversized, blocking}`. `--mermaid` nunca é bloqueado.
44
- - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
48
+ - **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo). **Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e o orquestrador repete a chamada.
45
49
  - **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`). Três medidas, cada uma **um aviso, nunca erro**, mapeado em `code_quality` da rubrica: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `T{n}`, `Gate N`, `spec.md`/`mandate.md`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1,5×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário). Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas. Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro. Fica fora do modo `light`. Excesso justificado não silencia o aviso: a justificativa mora na task (orçamento do `morph-plan` passo 5), e é lá que o avaliador a lê.
50
+ - **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--no-subset] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — a prova de mutação do Gate 3 **em lote**, para o avaliador independente: ele escolhe os mutantes, a ferramenta os roda todos numa chamada e imprime a seção `## Mutações` pronta (também em `4-review/mutation-section.md`). **Manifesto** (`--plan`, default `4-review/mutants.json`; JSON escrito com a ferramenta Write ou, só com Bash, por heredoc de delimitador entre aspas — e como o transporte do Bash reduz `\\` a `\` e decodifica escapes unicode — medido —, cada barra invertida do código vai como `{{BS}}`, que a ferramenta expande; JSON inválido sai com essa dica): `{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a","mode":"schemata|rebuild","note":"…"}]}`. `find` tem de **começar na `line`** e ser único nela; `arquivo:linha`, diff e os dois md5 do relatório saem da ferramenta, não da digitação. Dois `find` que se sobrepõem no mesmo arquivo são recusados; uma cláusula de predicado composto é um mutante próprio. **Método, em ordem:** (1) recusa arquivo com alteração não commitada (o fix se commita ANTES de mutar; `--allow-dirty` assume o risco); (2) snapshot dos bytes **fora da árvore** (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — uma rodada que morreu no meio se desfaz por `mutate --restore`; (3) rebuild **forçado** da árvore limpa e md5 da DLL: a baseline binária; (4) em .NET, antes, **uma triagem de um build só** compila a forma LITERAL de todos (o schemata guarda o texto original no `else`, então aceitaria um mutante cuja forma literal não compila — o `out var` removido e usado adiante); quem falha nela vai para rebuild real, que o julga (`build-error`, nunca morto); depois todo mutante `.cs` de expressão vira uma chave `MORPH_MUTANT` (`(Sw.On("M1") ? mutado : original)`) e **um único build** compila todos — a fonte é restaurada logo depois do build, então o disco fica mutado só durante ele; o que a chave não compila (declaração, `out var`, `const`, argumento de atributo) cai **sozinho** para rebuild real (build incremental por mutante, dizendo o erro do compilador); (5) **subconjunto primeiro**: os testes que a feature alterou contra a default mais os que citam os tipos mutados — a ferramenta escolhe, o avaliador não pode escolher o teste que "sabe" que quebra; vitest/jest usam `related`; (6) **sobrevivente do subconjunto é sempre refeito com o mutante compilado de verdade e a suíte INTEIRA** — "sobreviveu" só sai desse caminho, então o escopo rápido pode errar só para o lado do custo, nunca do veredito. **Paralelismo (`--jobs <n>`, default 2):** as rodadas do escopo rápido rodam em N processos sobre o MESMO binário de schemata — a chave é uma variável de ambiente, então não há cópia da árvore nem segundo build; cada worker toma um slot `heavy` livre e, sem slot livre, a rodada degrada para sequencial em vez de esperar. A rodada-base roda em N cópias ao mesmo tempo, e falha que só aparece sob concorrência entra na base (não vira morte). A suíte inteira nunca é paralelizada (já ocupa a máquina), e suíte que compartilha banco ou arquivo entre processos pede `--jobs 1`. Suíte no Microsoft.Testing.Platform (`global.json`) é recusada (exit 2): o veredito e o motivo saem do TRX do VSTest. **Veredito por mutante:** `killed` = falha **nova** (ausente na rodada-base do mesmo escopo) com o motivo literal — nome do teste + primeira linha da mensagem, do TRX. **Na suíte inteira a falha tem de se repetir:** a ferramenta refaz só os testes que falharam (até 5, pelo método, sobre o mesmo binário) e, se nenhum falhar isolado, mais uma rodada da suíte inteira (falha que só existe com o contexto da suíte continua morte); passar nas duas é instabilidade — o mutante fica `survived` com a razão dita (medido: um teste de host sem relação com o mutante o "derrubou" uma vez em 7 rodadas); a repetição que não termina é `inconclusive`; morte no subconjunto não é repetida. `survived`; `build-error` (o mutante nem compilado de verdade compila: não é morte); `inconclusive` (testhost caiu, estourou o tempo ou nenhum teste executou: não é morte nem sobrevivência, segue para a suíte inteira). A contagem carrega o escopo: **`≥K de N (subconjunto)`** — piso, não total — ou **`K de M (suíte inteira)`**. **Reversão:** bytes do snapshot com md5 conferido por arquivo, sempre por **escrita nova** — devolver o mtime antigo (`os.utime`) deixa o MSBuild achar o binário do último mutante em dia e a rodada-base roda sobre ele; ao fim, rebuild forçado e a DLL é comparada (md5) com a construída antes do 1º mutante (projeto de build não determinístico: prova de "recompilada depois da restauração e sem a chave"), e `git status --porcelain` tem de sair igual ao de antes. **Exit:** `0` rodou (sobrevivente é achado, não erro), `1` falha (a árvore já não compilava antes da mutação), `2` uso (manifesto inválido, arquivo sujo, nenhuma suíte roda o arquivo), `3` semáforo ocupado, `4` **inseguro** (arquivo não restaurado, árvore diferente ou binário que não bate — não avalie antes de resolver). Segura **um** slot `heavy` do início ao fim.
46
51
  - **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
52
+ - **`morph-spec heavy status [--json]`** e **`morph-spec heavy run [--label <x>] -- <comando…>`** — semáforo de nós pesados **por usuário**: os slots vivem em `%LOCALAPPDATA%morph-specheavy` (Windows) ou `$XDG_STATE_HOME/morph-spec/heavy` (`~/.local/state/…`), compartilhados por **todas** as sessões da máquina, com `heavy.slots` (2) vagas. **Quem toma slot:** build, testes (inclusive `--linux`) e e2e do `verify` — um slot por `verify`, adquirido só imediatamente antes do primeiro spawn real (nó pulado não adquire e a espera fica fora do `durationMs`) —, o `e2e up` (só durante a subida da stack) e o `heavy run`, que roda qualquer comando sob slot e repassa o exit code (`127` se não conseguiu executá-lo; `npm`/`npx`/`pnpm`/`yarn` viram `.cmd` no Windows). `heavy status` é **somente leitura**: quem segura cada slot (pid, label, feature, nó, idade), a limpeza exclusiva do build-server se houver e a RAM livre. **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo de `heavy.minFreeMemGb` (4), espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo). RAM baixa espera **com ou sem** holder de slot: a pressão costuma vir de fora (vmmem do WSL, IDE), e "sem holder → roda" reproduziria o "roda e morre". **Estouro = exit 3 (`busy`), nunca vermelho:** `verify --json` sai `{ status: "busy", persisted: false, busy: { reason, holders, freeGb, waitedMs } }`, **sem stamp e sem evento `Verify`** (nada foi verificado, e fila não é gate verde nem vermelho); `e2e up` e `heavy run` também saem 3. Quem orquestra repete a chamada — nas skills, exit 3 não conta como tentativa e não dispara o loop de correção. **Slot vago:** pid morto, ou idade acima de `heavy.maxHoldMinutes` (90, contra reuso de PID); Ctrl-C deixa o arquivo, que o pid morto cobre. Um filho herda o slot por `MORPH_HEAVY_LEASE` e nunca pede um segundo. **Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado. Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`. **Fim do trabalho pesado:** no fim do `verify` de escopo **feature** (nunca no de task: cada task pagaria compilador frio), no `e2e down` e no `doctor --procs --prune`, roda `dotnet build-server shutdown` **só** se o projeto é .NET, `heavy.shutdownBuildServer` está ligado, nenhum slot está preso **e** nenhum `dotnet build|test|publish|msbuild` está vivo — sub-agents e IDE não passam pelo semáforo, então semáforo vazio não basta. Com `heavy.enabled: false` nunca roda: sem semáforo, nada prova que outra sessão não está compilando. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`. `heavy.disableNodeReuse` põe `MSBUILDDISABLENODEREUSE=1` enquanto o nó pesado roda e restaura depois. O `--linux` rotula o container (`morph-spec.verify=<runId>`, `morph-spec.root=<hash da raiz>`) e o remove **por label** no fim, nunca por nome; o `e2e down` usa `--remove-orphans`. O `verify` **não** derruba a stack e2e (`keepUp` é por desenho). **Bash cru (`heavy.gateBash`, opt-in, padrão `false`):** o `dotnet build`/`dotnet test`/`npm test` de um sub-agent pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda. `heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`. **Não reescreve o comando:** a doc do Claude Code avalia as regras de permissão contra o comando **reescrito**, e o `allow` que acompanha um `updatedInput` aprovaria o que o prompt perguntaria; por isso não há reescritor (o `allow` não atropela regra `deny`/`ask`, mas a reescrita esconderia o comando delas). **O que a vaga não cobre:** ela não tem processo vivo para vigiar (o hook sai na hora), então vence por **TTL** — o `timeout` da chamada, no máximo o da plataforma, mais 30 s — e uma chamada recusada por regra de permissão ou interrompida pelo usuário, que não dispara `Post*`, segura o slot até lá. O TTL conta desde o `PreToolUse`, que roda **antes** do prompt de permissão: se a aprovação demora mais que o `timeout` + 30 s, a vaga vence com o comando ainda por rodar e outra chamada pode tomá-la (sobreassinatura, do lado seguro). Chamadas de Bash de um mesmo lote paralelo rodam **em série** no Claude Code (medido na 2.1.285: o `Pre` da 2ª só rodou depois do `Post` da 1ª), então um agente não fica esperando uma vaga que só a própria chamada anterior liberaria. `run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula 6 dos templates de dispatch (`heavy run`) segue valendo para eles. **Medido** com `claude -p` real e a chain instalada: fila cheia recusa após a espera e não mexe na vaga alheia; a vaga está presa durante a chamada e volta vazia depois, com exit 0 e com exit 1. Custo num Bash **leve**: ~1,5 ms na chain Pre (mediana 61,4 → 62,9 ms, medido intercalado) e ~0 na Post; os módulos do semáforo só carregam quando o comando é pesado. O núcleo (`heavy-config`, `heavy-holders`, `heavy-slots`) mora em `framework/hooks/shared`, porque hook não importa `src/`.
47
53
  - **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias. Uma linha `.git/morph-spec (<tamanho> · N backup(s) do finish)` mostra o que o morph-spec guarda dentro do `.git` (backups do finish, registros de porta e de claim), que não aparece em `git status`: `✓` normalmente, `⚠` quando há backup legado esperando `--prune`, backup fora da retenção ou `.env*` além do prazo curto (com o comando de poda) — nunca `✗`, e nenhuma linha quando não há nada guardado.
48
54
  **Personas (`.claude/agents/morph-*.md`).** São artefatos derivados de `.morph/framework/agents.json` — nunca editados à mão. O `doctor` padrão valida o `agents.json` (inclusive o `maxTurns`: inteiro ≥ 1 ou ausente — `0`, negativo, fracionário, string ou `null` é **erro** nomeando persona e valor, porque o installer recusa gerar essa persona — e o `surfaces`: `{ "<superfície>": { when, standards } }` com id kebab-case, `when` e lista não vazios, e nenhum standard repetido da lista universal `standards`; inválido é **erro** pelo mesmo motivo). Com `surfaces`, o corpo da persona ganha `## Standards by surface`: cada superfície com o seu `when`, e a instrução de ler a lista universal mais só a da superfície da tela — a que o checklist de tela do mockup nomeia em `Superfície`. O `--full` compara cada `.md` com o render atual e, no drift, diz **qual campo** divergiu por arquivo — `morph-x.md (maxTurns)` é edição à mão do teto; `(body)` depois de um upgrade da CLI é só regeneração pendente. Os grants `mcp__*` são conferidos contra o registry do **pacote**, o mesmo que o installer leu (a cópia instalada não tem `mcp/`). A linha `tools:` é allowlist estrita: toda persona recebe também `Skill` e `ToolSearch` (sem eles não invoca skill nem carrega MCP adiado), e cada MCP vira um grant por grafia de `serverNames` do registry — o mesmo servidor chama `mcp__context7` no `.mcp.json` e `mcp__plugin_context7_context7` como plugin, e um grant só casa com a própria grafia. O campo `maxTurns` é o teto de turnos do subagent gerado: por persona no `agents.json`, `20` quando ausente, e o corpo da persona o anuncia ao sub-agent junto com o turno de checkpoint (~70%) — o sub-agent não vê o próprio frontmatter. O valor vem do `agents.json` do **pacote**: `update` recopia o arquivo e regenera os `.md`, então editar a cópia em `.morph/framework/` não muda a instalação — só faz o `--full` acusar drift.
49
55
  **Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
50
56
  **Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando; branch `morph/*` cujo diff nunca tocou `.morph/features/{f}/` nem `.morph/archive/{f}/` — um PR só de docs — não é acusada, e diff ilegível ou truncado no teto do `gh` mantém o aviso). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
51
57
  Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado), e `--ai` para a auditoria de projeto .NET com agentes.
52
- - **`morph-spec doctor --ai`** — auditoria de um projeto **.NET com agentes**, em superfície própria: **não entra no `--full` nem no `doctor` default**. O renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` (não tem o ramo `skip → ○`), então uma checagem de IA lá seria vermelha mesmo dizendo "não verificado"; e cinco linhas `○` permanentes no comando mais rodado do dia é ruído com custo e sem informação. Combinar `--ai` com `--full`/`--mcp`/`--worktrees` é **erro de uso**: exit 1, sem rodar nada — cada superfície tem o seu relatório e o seu código de saída.
58
+ - **`morph-spec doctor --ai`** — auditoria de um projeto **.NET com agentes**, em superfície própria: **não entra no `--full` nem no `doctor` default**. O renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` (não tem o ramo `skip → ○`), então uma checagem de IA lá seria vermelha mesmo dizendo "não verificado"; e cinco linhas `○` permanentes no comando mais rodado do dia é ruído com custo e sem informação. Combinar `--ai` com `--full`/`--mcp`/`--worktrees`/`--procs` é **erro de uso**: exit 1, sem rodar nada — cada superfície tem o seu relatório e o seu código de saída.
53
59
 
54
60
  **Cinco checagens.** `pin-drift` (o que o `.csproj` declara × o que `framework/ai-pin.json` fixa), `removed-api` (símbolos renomeados antes do 1.0 GA do `Microsoft.Agents.AI`), `pricing-missing` (todo alias do registry de modelos tem tarifa?), `api-per-alias` (todo alias declara `api`?) e `prompt-source` (as `Instructions` do agente têm fonte declarada?).
55
61
 
56
62
  **Só `pin-drift` produz `error`, e só na faixa flutuante** — é a única classe com prejuízo medido: uma faixa `2.*` sobe sozinha para uma versão com assinatura mudada e o `MissingMethodException` chega **depois** de a chamada ao modelo ser paga. Versão fixa ≠ pin é `warn` nomeando as duas versões; pacote de IA presente no projeto e ausente do pin é `warn` descrito como **lacuna do pin**, nunca como defeito do projeto. As outras quatro checagens informam e nunca travam. **Exit code:** 1 com ao menos um `error`; 0 caso contrário — `warn` e `skip` nunca mudam o código de saída.
57
63
 
58
64
  **Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
59
- - **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`). Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível.
65
+ - **`morph-spec doctor --procs [--prune] [--json]`** — o que o morph-spec e as suas sessões deixam de pé na máquina, em superfície própria: **não entra no `--full` nem no default** (mesmo motivo do `--ai`), e combinar com `--full`/`--mcp`/`--worktrees`/`--reset`/`--ai` é **erro de uso**. Sem `--prune` é **somente leitura**. **Processos:** lista `node`, `dotnet`, `testhost`, `esbuild` e Chrome headless de automação (o seu Chrome comum não entra) **sem pai vivo**, com idade e RSS. Órfão = pai ausente da tabela **ou criado depois do filho** (PID reaproveitado). **Nunca é candidato à poda:** processo com menos de 15 min ou de idade desconhecida; linha de comando que casa com **qualquer MCP configurado** (`.mcp.json`, `~/.claude.json` do projeto e do usuário, `.mcp.json` dos plugins habilitados), mesmo órfão, porque no Windows a cadeia `claude→cmd→npx→node` perde o elo quando o `cmd` do meio morre; o worker de refresh do grafo; e os nós de build (MSBuild `/nodemode`, VBCSCompiler), compartilhados entre sessões — esses saem por `dotnet build-server shutdown` quando ociosos, nunca por kill. `--prune` encerra **por PID** só o que sobrou, relendo o início e a linha de comando daquele PID imediatamente antes (PID reaproveitado é pulado); **nunca por nome de imagem** (`taskkill /IM node.exe` derrubaria os MCPs de todas as sessões). Exit 1 só se algum encerramento falhar. **MCP duplicado:** o mesmo servidor no `.mcp.json`/`~/.claude.json` e entregue por plugin habilitado sobe dois processos (`mcp__X` e `mcp__plugin_X_X`); o relatório só sugere remover do `.mcp.json` ou desligar o plugin. O `init` e o `mcp setup --auto` deixaram de recriar a duplicata: um plugin habilitado que entrega o servidor conta como instalado. **WSL:** `%USERPROFILE%\.wslconfig` ausente, ou sem `memory=` (seção `[wsl2]`) ou sem `autoMemoryReclaim=` (seção `[experimental]`), vira sugestão com o trecho e o `wsl --shutdown`; o morph-spec **nunca escreve fora do repositório**. O `--json` traz `procs`, `mcp`, `wsl` e, com `--prune`, `prune`. Listagem indisponível (PowerShell falhou, plataforma sem listador) sai **não verificado** com razão, nunca lista vazia.
66
+ - **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`). Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível. **Sobras de sub-agent:** achado `agent-worktree-leftover` (`info`, nunca falha): `reapable` (fix: `morph-spec worktree reap`) ou preservada com a razão (`dirty`, `exclusive-commits`, `no-usage-event`, `directory-missing`, `unknown`); agente ativo e agente dentro da graça não geram achado, e a árvore não é reportada de novo como `unintegrated-work`. O `--prune` também roda o `worktree reap` (`prune.agentReap` no `--json`).
60
67
  - **`morph-spec report-check <feature> [task...] [--before-eval] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache; relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns` — sai `report-incomplete` com `partial: true` e ação `resume`: retome o agente, NÃO carimbe DEGRADADO. O marcador é lido de propósito, nunca inferido da falta de score: o marcador de score é frouxo e um parcial que diga "auto-score: pendente (0-10 no final)" casaria com ele). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Vários ids (`report-check {f} T1 T2 --json`) auditam um batch paralelo numa chamada: sem task, o modo feature só audita tasks `done`, e as de um batch recém-voltado ainda não estão. **Bloco `tree`**, em toda saída e sem mudar veredito nem exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `markers` (linhas com `MUTACAO` em arquivo rastreado ou não, fora dos ignorados), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde (fora de repositório ou erro) — desconhecido, nunca árvore limpa. **`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch do avaliador, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`: os arquivos cujo conteúdo não é o da foto, inclusive o que foi commitado desde então. É assim que uma sessão que retoma depois de o avaliador morrer no meio vê a mutação que ficou; sem foto, `sinceBeforeEval` é `null`. É o que o orquestrador lê depois de um dispatch, em vez de `git status`/`git diff`/`git log`/`grep`, e o que cola no recado de retomada. Protocolo completo em MORPH.md §6.
61
68
 
62
69
  ## Suítes de teste (`project.tests[]`) e a divisão loop × suíte