@polymorphism-tech/morph-spec 8.43.0 → 8.44.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 (38) hide show
  1. package/CLAUDE.md +6 -4
  2. package/README.md +2 -2
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/docs/CLI.md +13 -8
  7. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  8. package/framework/rules/ai-agents-standards.md +1 -0
  9. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  10. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  11. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  12. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  13. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  14. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  17. package/framework/skills/core/morph-media/SKILL.md +1 -1
  18. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  19. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  20. package/framework/skills/core/morph-review/SKILL.md +1 -1
  21. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  22. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  23. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  24. package/framework/standards/STANDARDS.json +45 -1
  25. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  26. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  27. package/package.json +1 -1
  28. package/src/commands/project/worktree.js +34 -16
  29. package/src/commands/state/approve.js +2 -1
  30. package/src/commands/state/finish.js +4 -0
  31. package/src/commands/state/gate-check.js +2 -1
  32. package/src/commands/validation/verify.js +13 -21
  33. package/src/lib/e2e/compose-env.js +5 -2
  34. package/src/lib/git/evidence-staleness.js +39 -0
  35. package/src/lib/risk-detector.js +3 -3
  36. package/src/lib/tasks/eval-node.js +4 -34
  37. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
  38. package/src/lib/worktree/env-allocator.js +35 -5
package/CLAUDE.md CHANGED
@@ -137,11 +137,13 @@ When modifying framework components, verify the full scope of related files.
137
137
  | `framework/MORPH.md` | All core skills (cross-references), CLAUDE.md (slash command list), e o **teto de 38 400 chars** (`test/framework/resident-size.test.js`). MORPH.md vive colado no teto: linha nova ali só entra liberando pelo menos o mesmo número de caracteres **na mesma seção**, com o antes/depois medido — `MORPH_MD_LIMIT` não sobe |
138
138
  | a divisão loop × suíte (`verify {f} {task}` recorta sozinho; `--filter` força nós, `--full` desliga) | os QUATRO textos que a declaram e não podem discordar: `framework/docs/CLI.md` (seção "Suítes de teste" — a tabela canônica, a forma de `project.tests[]` e a subseção *O recorte por task*, com os limites), `morph-implement` §9, `morph-apply` §3a (o `verify` do loop e os `outputs` gravados ANTES dele — o recorte os lê) e `morph-review` (o Gate 3 roda **sem** filtro e `verify {feature}` nunca recorta). Uma superfície que ensine filtrar no Gate 3 devolve o F30 pela porta da frente; canário: `test/framework/verify-stack-docs-prose.test.js` |
139
139
  | `src/lib/worktree/ignored-triage.js` | `worktree.js` (`classifyIgnoredLeftovers`), `test/lib/worktree/ignored-triage.test.js`, e os canários de `test/commands/finish/merge-worktree.test.js` (e o pré-voo de `finish/dry-run.test.js`) que pinam "o exemplar único nunca morre" — mudar um bucket muda o que o `finish` apaga |
140
- | `src/lib/worktree/finish-backup.js` (backups do finish e a retenção) | os QUATRO pontos que escrevem ou leem `.git/morph-spec/finish-backup/`: `finish.js` (o shed da raiz e a poda do fim, `enforceBackupRetention`), `worktree.js` (o backup do desmonte e a poda do fim do `worktree remove`), `worktree-audit.js` (`auditFinishBackups` → `doctor --worktrees`) e `doctor.js` (a linha `.git/morph-spec` do padrão, nunca `✗`, e o `--prune`, a ÚNICA poda que inclui legado). `resolveBackupRetention` é a autoridade do `finishBackup.retention` de `framework/schemas/config.schema.json`: política que não se lê PULA a poda, nunca cai no padrão, porque a operação apaga. Backup sem manifesto é legado, e a poda automática nunca o apaga nem o conta. Um finish grava as duas pastas com o MESMO carimbo, e o carimbo da execução vai em `protectStamps`. Só `task/` é namespace. Apagar é PROVE-or-DECLINE, como a triagem de `ignored-triage.js` (que decide o que entra no backup) |
140
+ | `src/lib/worktree/finish-backup.js` (backups do finish e a retenção) | os QUATRO pontos que escrevem ou leem `.git/morph-spec/finish-backup/`: `finish.js` (o shed da raiz e a poda do fim, `enforceBackupRetention`), `worktree.js` (o backup do desmonte, o shed do esqueleto da raiz de feature já arquivada — `clearRootWorktreeRegistration`, que só commita registro RASTREADO — e a poda do fim do `worktree remove`), `worktree-audit.js` (`auditFinishBackups` → `doctor --worktrees`) e `doctor.js` (a linha `.git/morph-spec` do padrão, nunca `✗`, e o `--prune`, a ÚNICA poda que inclui legado). `resolveBackupRetention` é a autoridade do `finishBackup.retention` de `framework/schemas/config.schema.json`: política que não se lê PULA a poda, nunca cai no padrão, porque a operação apaga. Backup sem manifesto é legado, e a poda automática nunca o apaga nem o conta. Um finish grava as duas pastas com o MESMO carimbo, e o carimbo da execução vai em `protectStamps`. Só `task/` é namespace. Apagar é PROVE-or-DECLINE, como a triagem de `ignored-triage.js` (que decide o que entra no backup) |
141
+ | a nota `memoryNote` do `finish` (`MEMORY_NOTE` em `finish.js`) | os DOIS leitores da mesma frase: `framework/docs/CLI.md` (entrada do `finish`) e `test/commands/finish/memory-note.test.js`. A nota é carimbada no **`emit`**, ponto único por onde passa todo `finished: true`; o canário do `--pr` existe por isso: aquele payload é montado À MÃO, fora do `mergeOutput`, então mover a nota para o construtor do merge deixa o teste do `--merge` verde e só ESSE morde (medido por mutação). A frase nomeia o que o repositório NÃO guarda; medir só a presença do campo aprovaria uma nota vazia |
141
142
  | `src/lib/git/pr-index.js` | os QUATRO leitores do mesmo fato "esta branch tem PR": `fleet-scanner.js` (coluna PR), `branch-classifier.js` (o que NÃO apagar), `worktree-audit.js` (o que NÃO sugerir remover) e `pending-prs.js` (a síntese "aguardando merge"). `null` é UNKNOWN, nunca "sem PR" |
142
143
  | `src/lib/git/branch-classifier.js` | `src/commands/project/branch.js`, `worktree-audit.js` (`unintegratedWork` reusa `classifyBranch` — o `doctor` e o `branch prune` não podem discordar sobre a mesma branch), `test/commands/branch-prune.test.js`, e `lifecycle-audit.js` (`abandoned-scaffold` conta commit próprio pelo mesmo `resolveBaseRef` + `commitsAhead`) |
143
144
  | `src/lib/git/pending-prs.js` | os TRÊS consumidores da mesma síntese "aguardando merge": `list.js` (linha sintetizada), `fleet-scanner.js` (entrada `pr-pending`) e `doctor.js` (`auditPullRequests`). Uma feature visível numa superfície e invisível na outra é o bug que este módulo existe para impedir |
144
145
  | `src/lib/git/gh.js` | `pr-index.js`, `pr-files.js` e `finish.js` — as DUAS formas de invocar o `gh` viraram uma; uma segunda cópia de `resolveGh` volta a tornar o caminho de sucesso do índice intestável |
146
+ | `src/lib/git/evidence-staleness.js` (`treeMovedSince`, "a árvore andou desde o commit da prova") | os DOIS leitores do mesmo fato: `gate-check.js` (`verificationStale`, o carimbo do `verify`) e `approve.js` (`reviewStaleness`, o `Reviewed-SHA` do review-report). Uma régua só: comparar SHA com HEAD fazia todo commit de estado (o `feature.json` do próprio verify, `tasks.json`, `recap.md`) invalidar a prova sem nenhum byte de código diferente. Conta tudo fora de `.morph/` e, dentro dele, só o `config.json` — sem `expectedCount`, que a rodada de baseline escreve e não é evidência; um comando de teste trocado conta. `--no-renames` é load-bearing: um arquivo movido para dentro de `.morph/` aparece como criação lá e a deleção fora some. SHA que o git não resolve = andou (a resposta de antes); sem SHA ou sem HEAD = não andou (fail-open). **Não reuse `treeFingerprint` para isto:** ele hasheia o `HEAD`, então um commit só de `.morph/` o muda. Canários: `test/lib/git/evidence-staleness.test.js` (mutação: sem a normalização e sem `--no-renames` cada um derruba um teste) e os de `gate-check.test.js`/`approve.test.js` |
145
147
  | `src/lib/git/tree-fingerprint.js` (a impressão da árvore) | `verify.js` (grava `fingerprint` no evento `Verify`; `findRepeatedVerify`, de `verify-event.js`, compara com o último verify de feature COMPLETO — rodada com `persisted: false` nunca é a prova que uma completa substitui) e `metrics.js` (`verify.unchangedRepeats`, pela mesma regra). `.morph/` fica fora, exceto `config/config.json`: o próprio verify escreve feature.json e logs a cada rodada, e contá-los faria duas rodadas sobre o mesmo código parecerem diferentes. `repeatOf` é informação e nenhum gate o lê: ligá-lo ao Gate 3 é aceitar a prova de uma rodada anterior no lugar da atual |
146
148
  | `src/lib/git/tree-state.js` (o bloco `tree` do `report-check`) | `report-check.js` (toda saída, inclusive o batch `report-check <f> T1 T2`, e a foto `--before-eval` em `4-review/tree-before-eval.json`) e as skills que leem os campos: `morph-implement`/`morph-apply` (pós-dispatch e retomada do PARCIAL) e `morph-review` (higiene contra `tree.sinceBeforeEval`). Campo renomeado aqui sem as skills é instrução que lê `undefined`: `test/framework/report-check-wiring.test.js` confere que todo `tree.*` citado existe na saída real. A foto compara CONTEÚDO (blob por arquivo), não o `git status`: a mutação que o avaliador commitou também aparece. Fora de git o bloco é `null`, nunca árvore vazia. `MUTATION_MARKER` tem a mesma grafia que as skills mandam gravar |
147
149
  | `src/lib/worktree/feature-truth.js` | The six readers of the same "which tree owns this feature" predicate: `fleet-scanner.js`, `list.js`, `handoff.js`, the mutator guard (`approve.js`/`advance.js`/`score.js`/`gate-decision.js`/`gate-check.js`), `verify.js` (`decidePersistence`, asked from the PRIMARY ROOT — asked from cwd, step 4 would make any scratch tree carrying a `feature.json` persist) and the dashboard's `features-scanner.js` (only for ACTIVE features — it spawns git — and a worktree owner without the feature files is shown as `rootStale`, never silently replaced by the root skeleton) |
@@ -151,8 +153,8 @@ When modifying framework components, verify the full scope of related files.
151
153
  | `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
154
  | `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
155
  | `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 |
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 |
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` |
156
+ | `--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` (`baselineStampBlock`: só a rodada que GRAVOU a baseline persiste o stamp — sem gravação, o `reason` nomeia o nó), `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 |
157
+ | `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 se resolve em UM lugar, `resolveAiPin` (`src/lib/ai/pin-drift.js`, o mesmo do `doctor --ai`): `.morph/framework/ai-pin.json` do projeto e, sem ele, o do pacote da CLI. O `update` nunca copia o pin, então o fallback é o caso comum — um leitor próprio do eval deixou `MORPH_EVAL_PIN` sem ser injetado em todo projeto instalado, e com ele a régua do carimbo desligada sem aviso. Subir o pin do pacote reprova (`eval-cache-stale-pin`) toda cache carimbada no anterior: é o efeito, não um defeito. **`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` |
156
158
  | `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` |
157
159
  | `src/lib/graph/**` | `test/lib/graph/*.test.js`, the committed fixture `test/fixtures/graph/namespace-hierarchy.graph.json` (ADR-003 regression — never hand-edit it), `doctor.js` (`codebase graph` check), `architecture-scanner.js` (centrality section), `setup-infra.js` step 18b. `isTestPath` (`sanitize.js`) é o predicado ÚNICO de teste: `god-nodes.js`, `recoverDispatch`, `graph status\|explain` e `symbol-scanner.js` (`isTestFile`, que o `code-budget` também lê) o importam, e uma segunda régua faz o grafo e o validador discordarem sobre o mesmo arquivo. A fixture `test-doubles.graph.json` saiu do graphify real (passo a passo em `docs/specs/graphify-integration/fixture-test-doubles/`) — regerar, nunca editar à mão |
158
160
  | `framework/hooks/shared/graph-freshness.js` (a regra única de "grafo em dia") | os três lados que perguntam: `graph-refresh.js` (decide se roda e se o disparo solta processo), `graph-loader.js` `getFreshness` (`graph status\|explain\|affected` e o `doctor`) e `graph build\|refresh` (gravam a rodada em `<git-common-dir>/morph-spec/graph-refresh-state.json`). O registro da rodada existe porque o `graphify update` 0.9.50 não recarimba `built_at_commit` quando a topologia não muda: sem ele, o grafo parecia velho para sempre e cada disparo rodava o graphify de novo (~80 s no MORPH_GHLBrain). As regras do `.graphifyignore` entram por CONTEÚDO (hash sem comentário, BOM e CRLF), nunca pelo relógio, e `ignoreTextOf` é a ÚNICA leitura de "as regras desta árvore": numa worktree sem arquivo próprio, as da raiz — as mesmas que o `graph refresh` põe ali durante a rodada (`prepareIgnore` em `build.js`). O arquivo temporário leva `TEMP_IGNORE_MARKER` na 1ª linha; nas duas triagens de sujeira (`finish` e `worktree remove`) `withoutLeftoverIgnore` o tira da conta ANTES das recusas e `dropLeftoverIgnore` o apaga só DEPOIS delas — apagar antes é mutação atrás de uma precondição. Mudar a grafia do marcador num lado só devolve o `worktree-dirty` de uma rodada morta no meio. Mora em `hooks/shared` porque hook não importa `src/` |
@@ -172,7 +174,7 @@ When modifying framework components, verify the full scope of related files.
172
174
  | `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` |
173
175
  | 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) |
174
176
  | `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` |
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 |
177
+ | `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. **Alias por projeto** (`e2e.portEnvNames`, `resolvePortAliases`): é só outro NOME para as mesmas quatro portas, então não cria offset nem toca o template; entra pelo `portLayout` e por isso os dois escritores o levam, o `layoutKeys` do `compose-env.js` o protege do `.env.e2e` herdado, e o hook `worktree-env-inject` injeta toda chave do manifesto (menos `MORPH_FEATURE`) em vez de uma lista literal que divergiria |
176
178
  | `src/lib/tasks/test-nodes.js` | os DOIS leitores de `validateTestNodes` — `verify-runner.js` (fail-closed: nó inválido avermelha o gate) e `doctor.js` (`doctor --full`, camada 3 do contrato do spec §2.2: WARN, e **pula sem imprimir nada** sem `config.json` ou sem `project.tests`, senão quebra `test/commands/doctor.test.js:46`). Uma segunda noção de "nó válido" faz o mesmo config passar num lugar e falhar no outro. `morphConfigPath` deriva de `CONFIG_FILE_NAME` (`core/state/state-manager.js`), a mesma constante de `readMorphConfig` — o JSDoc promete "one spelling" e agora é literal. O campo `groups` (#87): `TASK_GROUPS` é o MESMO enum do `group` de `tasks.schema.json` e de `config.schema.json` (canário em `test/lib/tasks/test-nodes.test.js`), `RUNNER_DEFAULT_GROUPS` dá o default por runner e `effectiveGroups` é o que `test-scope.js` lê — `null` é nó sem grupo, nunca palpite. `discoverFrontendPackages` (a união do fallback de `resolveTestNodes` e o WARN do `doctor --full`) varre as pastas de `PACKAGE_LOCATIONS`, de `src/lib/detectors/index.js` — uma segunda lista de pastas é como dois detectores começam a discordar |
177
179
  | `src/lib/worktree/docker-audit.js` | `worktree-audit.js` (`auditWorktrees` concatena os achados e é quem PASSA as features conhecidas — um segundo scan aqui discordaria sobre o que é órfão), `doctor.js` (a classificação `info`/`warn` = não-falha em `doctorWorktreesCommand`; e `samePathCase`, a regra de caixa de path que o WARN de frontend do `doctor --full` e o `spelled` de `test-scope.js` reusam — uma segunda cópia dela foi removida), `env-allocator.js` (`allocatedFeatures` é read-only de propósito: `allocateEnvBlock` ESCREVE, e uma auditoria que aloca inventa o estado que reporta), `test/lib/worktree/docker-audit.test.js`. Nunca no `doctor --full` (D10): `test/commands/doctor.test.js:46` exige zero `✗` neste repo |
178
180
  | `src/lib/risk-detector.js` | os DOIS lados da mesma decisão de gate: `gate-check.js` (`collectSignals` COLETA do disco; nenhum sinal que descreva um FATO pode entrar em `OVERRIDABLE_SIGNAL_KEYS` — `gate`/`workType`/`e2e*` são o precedente), `src/commands/validation/verify.js` (o stamp `verification` é a ÚNICA coisa que o gate lê: campo novo aqui só vira sinal se o stamp o gravar), `framework/schemas/feature.schema.json` (`verification` é `additionalProperties: false`), `framework/MORPH.md` §5 (tabela de sinais — ela se declara fonte única) e `framework/docs/CLI.md`. Política escrita no coletor cria duas fontes para o mesmo fato — foi assim que `gate-check` e `approve` divergiram uma vez sobre o score do Gate 3. **`verificationScope` é sinal, não auditoria**: a dispensa de e2e (`project`/`scope`) só conta vinda de um stamp de escopo `feature` — um `verify {f} {task}` de task `group: docs` carimba a razão de dispensa e compraria o Gate 3 da feature inteira. **E o escopo de task pausa o ship por si só** (#87): no `review`, `verificationScope` string ≠ `'feature'` pausa ANTES do bloco de e2e, mesmo com todos os nós verdes — o recorte automático faz TODO `verify {f} {task}` rodar menos que a suíte; `null` (stamp legado) não pausa por esta regra. O par mora em `verify.js`: `--filter` de escopo de feature não persiste o stamp (`reason: 'filtered'`), nem `--skip-tests`/`--skip-build` (`'skip-tests'`/`'skip-build'`, #113 — nó pulado não reprova o rollup e nenhuma regra do gate lê `tests`/`build` pulados; a lista é `PARTIAL_RUN_FLAGS`) |
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.43.0
6
+ **Version:** 8.44.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.43.0 by [Polymorphism Tech](https://polymorphism.tech)*
382
+ *morph-spec v8.44.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.43.0",
3
+ "version": "8.44.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.43.0 by Polymorphism Tech*
190
+ *morph-spec v8.44.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.43.0 by Polymorphism Tech*
252
+ *morph-spec v8.44.0 by Polymorphism Tech*
@@ -9,7 +9,7 @@
9
9
  - **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando). **No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
10
10
  - **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta). Ao encerrar a **uiux**, a assinatura `auto` é recusada (exit 1) pelo mesmo predicado do `approve` e do `gate-check uiux`: checklist de tela incompleto no `1-design/mockups.md` — toda `## Tela:` com o `### Checklist de tela` respondido item a item (`src/lib/specs/screen-checklist.js`), com a `Superfície` sendo uma das `surfaces` da `ui-designer` no `agents.json` instalado. Destravam a decisão gravada com `pause: false` (`gate-decision`, e só para o mockup que ela viu) e a assinatura humana (`--approver <nome>`); `--force` não dispensa o checklist, como no `approve`. Quem avança sobre o checklist incompleto com `--approver` vê o que ficou faltando. Sem `mockups.md`, ou com o mockup no formato antigo (telas `## S001:`, sem `## Tela:`), não há o que cobrar.
11
11
  - **`morph-spec score <feature> <task> [score] [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. **Duas recusas duras (exit 1), nunca um registro silencioso:** `<score>` em branco ou ausente é **recusado, jamais gravado como `0`** (o `Number('')` que valia `0` gravava um zero fabricado — e era esse zero que o Gate 3 lia depois, como `evalScore: 0 / evalScoreSource: taskScores-proxy`); e `<task>` fora de `^T\d+$` é **recusado** em vez de aninhado (`T3.2` virava `taskScores.T3["2"]` — inválido contra `feature.schema.json` e invisível ao proxy). O `[score]` aparece opcional só para que o valor ausente chegue ao comando e receba uma mensagem que nomeia o argumento e a ordem posicional — ele continua obrigatório. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
12
- - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (rubrica `evals/rubrics/contracts.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
12
+ - **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (rubrica `evals/rubrics/contracts.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
13
13
  - **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
14
14
  - **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
15
15
 
@@ -22,16 +22,16 @@
22
22
 
23
23
  ## Worktree / Fleet
24
24
 
25
- - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
25
+ - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação. **Toda saída de sucesso carimba `memoryNote`**: a frase que nomeia o que o fechamento NÃO deixa registrado no repositório — decisão do usuário, pendência a observar em produção, custo medido. O commit de archive, o `decisions.md`, os use cases promovidos e o `git log` já guardam o resto, e memória de sessão escrita por cima desses fatos nasce redundante e envelhece sem revisão. Recusa não a carimba: não há fechamento sobre o qual gravar nada.
26
26
  - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
27
27
  - **`morph-spec usecase check <feature> [--json]`** — o ciclo de vida do acervo contra a feature, rodável antes de existir `tasks.json` (o `spec.md` congela no Gate 2, então é aqui que uma linha de revogação errada ainda tem conserto): task cujo `usecase` está `superseded` (no acervo, na cópia da feature ou revogado pelo próprio `spec.md`), frontmatter de supersessão malformado (`superseded` sem `superseded_by` UC-{nn}, `superseded_by`/`scope` sem `status: superseded`) e linha de revogação que o `finish` não aplicaria (id malformado, UC fora do acervo, substituto inexistente, ADR fora do índice). Avisos, exit 0. Com `tasks.json`, roda também o cruzamento task × use case do `dag` — e sai 1 onde o `dag` sairia (UC inexistente, schema inválido). `--json`: `{feature, useCases, lifecycle: {warnings}}`.
28
28
  - **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
29
29
  - **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). `remove` recusa, antes de tocar em qualquer junction, quando o cwd do processo está no alvo (`cwd-inside-worktree`) **ou quando a sessão do Claude Code que o rodou está ancorada nele** (`session-anchored-in-worktree` — mesmo sinal do `finish`, acima); a recusa é o adiamento: feche a sessão e rode de uma aberta na raiz. Falha de remoção reporta a CAUSA real: um diretório travado vence o eco `is not a working tree` das tentativas seguintes, e `alreadyDeregistered` avisa quando o git já desregistrou a árvore e só o diretório vazio sobrou. **Fechar a feature** continua sendo `morph-spec finish` (acima).
30
30
  **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
31
31
  **Base da árvore de task.** Com `worktree.baseRef` ausente nas settings e o cwd de quem despacha numa branch `morph/{f}`, o hook passa `--base morph/{f}`: o sub-agent do apply nasce com o trabalho da feature (nada de `git merge --ff-only morph/{f}` como passo 0). `baseRef: "head"` usa o que o cwd tiver checado; `baseRef: "fresh"` explícito força a branch default. Com base `morph/{f}`, o `worktree.env` grava `MORPH_FEATURE={f}` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
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).
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`. **Um projeto cujo compose lê a porta por outro nome** (`${PG_PORT}` em vez de `POSTGRES_PORT`) declara `e2e.portEnvNames` no `config.json` (`{ "postgres": "PG_PORT" }`; chaves `web`, `api`, `postgres`, `redis`): o nome declarado sai com o **mesmo valor** em todo lugar do layout e, sendo layout, o `envFile` de credenciais não o sobrescreve (vale o mesmo aviso em stderr). Entrada que não dá para honrar (porta desconhecida, nome inválido ou reservado, nome repetido) é descartada e nomeada em stderr. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
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
- - **`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.
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. **Nada é commitado na branch principal por causa do esqueleto da feature na raiz:** o registro `worktree` é apagado do `feature.json`, e só um registro que já estava **rastreado** vira commit. O esqueleto não rastreado de uma feature já arquivada na raiz é apagado com backup (`shedBackup`); o de uma feature sem archive fica como está.
35
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
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
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.
@@ -45,7 +45,7 @@
45
45
  ## Validação
46
46
 
47
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.
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.
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`. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo). **Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e o orquestrador repete a chamada.
49
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
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.
51
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.
@@ -140,7 +140,10 @@ mesmo código de saída. Por isso ele entra como um **sétimo valor de `runner`*
140
140
  `MORPH_EVAL_PIN`. O caminho do sumário é escolhido **pelo CLI** (arquivo temporário) para que dois nós
141
141
  de eval no mesmo `verify` não colidam, e `MORPH_EVAL_PIN` leva só os pacotes que decidem o **formato**
142
142
  da cache — a família `Microsoft.Extensions.AI` — para que subir um pacote de provider não invalide uma
143
- árvore por nada.
143
+ árvore por nada. O pin vem do `.morph/framework/ai-pin.json` do projeto e, sem ele, do pacote da CLI (o
144
+ `update` não o copia, então o normal é ser o do pacote — o mesmo que o `doctor --ai` usa). Subir o pin
145
+ do pacote faz toda cache carimbada no pin anterior cair em `eval-cache-stale-pin`: regrave com
146
+ `verify --refresh-cache`.
144
147
 
145
148
  **Os estados da cache são julgados ANTES de o comando rodar**, e todos os quatro são `fail` nomeado,
146
149
  **nunca** `skip`:
@@ -258,8 +261,10 @@ fallback sintetiza exigiria inventar a declaração que o comando veio manter).
258
261
  linha de comando é erro de uso (exit 2): baseline é da frota inteira, e o escopo de task recorta nós e testes. Verde com gravação
259
262
  recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
260
263
  pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
261
- testes rodaram. E o comando **não** persiste o stamp `verification` — é manutenção de config, não prova
262
- de gate; não o use como atalho de Gate 3. **Consequência a conhecer:** um `expectedCount` digitado à
264
+ testes rodaram. A execução que **gravou** a baseline (verde e inteira) persiste o stamp `verification` como qualquer verify de
265
+ feature: ela mediu a suíte toda, e rodá-la de novo só para ter prova seria pagar duas vezes pelo mesmo
266
+ verde. Sem gravação (vermelha, parcial, contagem desconhecida) não há stamp, e o `reason` do `--json`
267
+ nomeia o nó que impediu. **Consequência a conhecer:** um `expectedCount` digitado à
263
268
  mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
264
269
  `--record-baseline` recusa — preservando o 9. **Não há flag de regravação forçada:** rebaixe ou apague
265
270
  o número à mão e então re-meça pelo comando. O caso oposto se conserta sozinho — número baixo demais
@@ -42,9 +42,6 @@ const LAUNCHER_RE = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start)\b|nex
42
42
  /** The command already chose its own port/project — respect it. */
43
43
  const SELF_CONFIGURED_RE = /(?:^|[\s;&|])(?:PORT|API_PORT|POSTGRES_PORT|COMPOSE_PROJECT_NAME|ASPNETCORE_URLS)=|--port\b|(?:^|\s)-p\s+\d|compose\b[^|;&]*\s-p\s/;
44
44
 
45
- /** Keys the layout owns; anything else in the manifest is left alone. */
46
- const LAYOUT_KEYS = ['COMPOSE_PROJECT_NAME', 'MORPH_PORT_BASE', 'MORPH_PORT_RANGE', 'PORT', 'API_PORT', 'POSTGRES_PORT', 'REDIS_PORT', 'ASPNETCORE_URLS', 'PLAYWRIGHT_BASE_URL'];
47
-
48
45
  /** POSIX single-quote, safe for Git Bash on Windows too. */
49
46
  function shq(v) {
50
47
  return `'${String(v).replace(/'/g, "'\\''")}'`;
@@ -59,7 +56,7 @@ function shq(v) {
59
56
  export function rewriteCommand(command, env) {
60
57
  if (!command || !LAUNCHER_RE.test(command)) return null;
61
58
  if (SELF_CONFIGURED_RE.test(command)) return null;
62
- const pairs = LAYOUT_KEYS.filter((k) => env[k]).map((k) => [k, env[k]]);
59
+ const pairs = Object.keys(env).filter((k) => k !== 'MORPH_FEATURE' && env[k]).map((k) => [k, env[k]]);
63
60
  if (!pairs.some(([k]) => k === 'PORT')) return null;
64
61
  // The web tier's URL is what the spec suite needs; derive it once here.
65
62
  if (!env.PLAYWRIGHT_BASE_URL) pairs.push(['PLAYWRIGHT_BASE_URL', `http://localhost:${env.PORT}`]);
@@ -50,6 +50,7 @@ stacks:
50
50
  | `ai-agents/conversational-agent-with-phases.md` | conversa com fases no turno — allow-list de tools, `ChatToolMode`, teto de tool calls |
51
51
  | `ai-agents/guardrails.md` | regra proibida vira código — guard de tool, pertinência, argumento não confiável, fail-open |
52
52
  | `ai-agents/testing-ai.md` | testar agente sem rede — `IChatClient` fake, harness de turno, golden de prompt |
53
+ | `ai-agents/tool-adapters.md` | tool de agente que carrega regra de negócio — a regra em `*Ops` com desfecho tipado, o texto do modelo no adaptador, teste em duas camadas e migração neutra |
53
54
  | `ai-agents/evals-with-cache.md` | eval de modelo/prompt no CI **sem credencial e sem rede**: cache de respostas commitada, dataset versionado, o que entra na chave de cache, troca de modelo e regressão de prompt |
54
55
  | `ai-agents/cost-and-budget.md` | custo em dinheiro — `UsageCostMiddleware`, tarifa por token, teto de orçamento |
55
56
  | `ai-agents/llm-runtime-defaults.md` | knobs de runtime da chamada — timeout, retry, `strict` JSON schema, temperatura/reasoning |
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
4
4
  version: core
5
5
  when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
6
6
  user-invokable: false
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -4,7 +4,7 @@ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha
4
4
  version: core
5
5
  when-to-invoke: quando a máquina está lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
6
6
  user-invokable: false
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-machine
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
4
4
  version: core
5
5
  when-to-invoke: quando a tarefa é PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisa decidir modelo, prompt e aprovação
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
4
4
  version: core
5
5
  when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when-to-invoke: quando precisar consultar um standard específico durante a implementação
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
4
4
  version: core
5
5
  when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.43.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux