@polymorphism-tech/morph-spec 9.2.0 → 9.3.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 (53) hide show
  1. package/CLAUDE.md +14 -78
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +7 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -3
  8. package/framework/MORPH.md +5 -8
  9. package/framework/agents.json +9 -6
  10. package/framework/commands/morph-status.md +2 -2
  11. package/framework/docs/CLI.md +8 -2
  12. package/framework/evals/review-checklist.md +3 -0
  13. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  14. package/framework/hooks/claude-code/core/worktree-context.js +23 -0
  15. package/framework/hooks/claude-code/statusline.py +3 -2
  16. package/framework/hooks/shared/gate-event.js +18 -0
  17. package/framework/hooks/shared/next-step.js +107 -0
  18. package/framework/hooks/shared/slice-progress.js +18 -5
  19. package/framework/hooks/shared/state-writer.js +6 -1
  20. package/framework/mcp/registry.json +31 -1
  21. package/framework/schemas/feature.schema.json +1 -0
  22. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  23. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  24. package/framework/skills/core/morph-apply/SKILL.md +4 -2
  25. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  26. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  27. package/framework/skills/core/morph-media/SKILL.md +1 -1
  28. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  29. package/framework/skills/core/morph-review/SKILL.md +1 -1
  30. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  31. package/framework/skills/core/morph-uiux/SKILL.md +2 -1
  32. package/framework/standards/STANDARDS.meta.json +1 -1
  33. package/framework/standards/ai-agents/setup.md +2 -0
  34. package/framework/standards/integration/mcp/mcp-tools.md +3 -1
  35. package/package.json +1 -1
  36. package/src/commands/observability/cost.js +12 -0
  37. package/src/commands/project/status.js +6 -68
  38. package/src/commands/state/advance.js +1 -1
  39. package/src/commands/state/gate-check.js +2 -2
  40. package/src/commands/state/gate-decision.js +4 -1
  41. package/src/commands/state/next.js +24 -0
  42. package/src/commands/state/report-check.js +3 -1
  43. package/src/commands/validation/verify.js +6 -1
  44. package/src/core/state/state-manager.js +4 -0
  45. package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
  46. package/src/dashboard/scanner/features-scanner.js +5 -2
  47. package/src/lib/generators/handoff-generator.js +11 -25
  48. package/src/lib/next-step.js +10 -0
  49. package/src/lib/risk-detector.js +18 -7
  50. package/src/lib/stack/stack-scan.js +13 -1
  51. package/src/lib/telemetry/metrics.js +46 -0
  52. package/src/lib/validators/verify-runner.js +1 -1
  53. package/src/utils/agents-installer.js +23 -0
package/CLAUDE.md CHANGED
@@ -116,84 +116,20 @@ When modifying framework components, verify the full scope of related files.
116
116
 
117
117
  ### Dependency Matrix
118
118
 
119
- | When you modify... | Also verify... |
120
- |----------------------------------|-------------------------------------------------------------------------------------------|
121
- | `framework/agents.json` | agents-installer.js (test/utils/agents-installer.test.js), doctor.js (REQUIRED_AGENTS). **`maxTurns` (ADR-004):** UMA régua de validade, `maxTurnsError` em `agents-installer.js`, com QUATRO leitores — `installAgents` (recusa antes de escrever), `checkAgentsDrift` (`invalid`), `validateAgentsJson` do `doctor` (error) e o Check 6 do `validate-framework.js` (`test/framework/validate-framework-agents.test.js`); uma segunda noção de "valor válido" faz o mesmo arquivo passar num e ser recusado no outro. Sem o campo, o frontmatter sai byte a byte igual ao de antes (canário COMPAT). O corpo gerado anuncia o teto ao sub-agent, e a cláusula 5 dos templates de dispatch depende disso. O drift lê o registry MCP do PACOTE (`mcpRegistryDir`): a cópia instalada não tem `mcp/`, e ler dela dava toda persona com `mcp` como stale numa instalação limpa. O marcador `PARCIAL` (1ª linha) que o corpo da persona manda gravar é lido por `PARTIAL_MARKER` de `framework/hooks/shared/slice-progress.js` (no `report-check`: `report-incomplete`, ação `resume`; no progresso: fatia `parcial`): mudar a grafia num lado só devolve o parcial lido como `ok`. **`effort`:** mesma régua única em quatro leitores (`effortError`); ausente, o frontmatter não ganha linha e o sub-agent herda o effort da sessão (COMPAT intacto). Implementadores e `scout` `medium`, menos a `ui-designer` (`high`, com `opus`: a qualidade visual é a entrega); `verifier`, `reviewer` e `security` sem o campo — o Gate 3 não é barateado. `reviewer`/`security` são read-only sobre o SHA (`git diff {base}..{sha}`, nunca a árvore): o corpo da persona diz isso, e é o que torna seguro o paralelo com a mutação do verificador (F46). **`surfaces` (ADR-005):** mesma régua única em quatro leitores (`surfacesError`), e `personaStandardIds` é a leitura única de "todo standard da persona" para o Check 1, o drift e os scanners do dashboard (`agents-scanner.js`, `standards-scanner.js`) — iterar só `standards` deixa id de superfície sem validar e a aresta do grafo sumida. Sem o campo, o `.md` sai byte a byte o de antes: `test/fixtures/agents/ui-designer-5.3.0.{json,md}` é a persona do 5.3.0 e o render do installer de main, comparados com `strictEqual` — nunca regere a fixture para fazer o teste passar. A chave da superfície é a que o checklist de tela do `morph-uiux` escreve em `Superfície` e que o `morph-plan` usa para somar a lista em `standards:` da fatia. **Forma do corpo (`buildBody`):** só a persona no formato do 5.3.0 — implementadora (Write/Edit), sem `effort` e sem `surfaces` (`legacyShape`) — mantém `## Domains` e a lista de standards sem a linha de busca; no `agents.json` atual NENHUMA cai nesse caso (todo implementador tem `effort`; `verifier`/`reviewer`/`security` são read-only), e o formato vive pela fixture COMPAT acima. Toda outra persona perde `## Domains`, ganha `STANDARDS_HINT` (`--find <id>` e `--show <id>#<âncora>`) no topo de `## Standards to consult on demand` e, com `neon` no `mcp` (hoje `ef-modeler` e `infra-engineer`), o aviso `NEON_CAUTION` (ensaio de migração em branch descartável; tool destrutiva só com confirmação humana). Canários: `test/utils/agents-installer.test.js` e `test/framework/maf-expert-persona.test.js` |
122
- | `framework/standards/**` | STANDARDS.json entry exists (o **digest** e o que `standards --find` devolve — corrigir a prosa e esquecer o digest e o caso mais comum), e o bullet do standard na rule `framework/rules/*-standards.md` que o roteia (resumo inline; `@`-import em rule resolve relativo ao arquivo da rule e, quando resolve, carrega em TODA sessão ignorando `paths:` — medido; canário `test/framework/rules-no-imports.test.js`), agents reference standard, .md file exists. **Bloco de codigo `csharp` num standard e afirmacao verificavel:** extraia com `node scripts/extract-doc-csharp.mjs <std.md> "## Secao" out.cs` e compile numa sonda com o pin + `TreatWarningsAsErrors` (sem ele `MEAI001` vira aviso e o build fica verde sobre codigo que o consumidor nao compila). Medido: um exemplo carimbado sem compilar tinha 3 erros independentes |
123
- | `isImplementer` (Check 6 de `scripts/validate-framework.js`) | toda persona com Write/Edit no escopo efetivo de tools tem de alcançar, pelos `standards`, as âncoras `comment-policy` e `test-budget` do STANDARDS.json (`IMPLEMENTER_ANCHORS`). Renomear a chave da âncora no registry derruba o `validate`, e persona implementadora nova sem os dois standards também. Installer e `doctor` não cobram isso de propósito: um `agents.json` instalado antigo continua valendo |
124
- | `scripts/generate-standards-registry.js` | Escreve DOIS arquivos: `framework/standards/STANDARDS.json` (o que o runtime lê: `id`, `name`, `path`, `category`, `stacks`, `tags`, `anchors`, `digest`) e o sidecar `STANDARDS.meta.json` (`aliases`, `verifiedAgainst`, `provedBy` por **id**, e `sharedAliases` no topo). REGENERE os dois, nunca patch textual. Os `aliases` do sidecar se preservam pelo id da entrada: renomear um id sem levar a chave do sidecar perde os aliases dele. `--check` compara os DOIS arquivos com o que geraria (drift em qualquer um reprova o `npm run generate:check`). As âncoras são validadas contra os headings pelo `headingSlugs` de `src/lib/standards/sections.js`, a mesma resolução do `--show`. `src/lib/standards/verified-against.js` (`parseVerifiedAgainst`/`deriveProvedBy`: `verifiedAgainst` e `provedBy` sao DERIVADOS a cada run e NAO entram em `PRESERVE_FIELDS`), `test/framework/standards-registry.test.js`. **Renomear um standard e uma PARADA, nao um exit 0**: path sumido + path novo faz o gerador recusar nomeando os dois conjuntos e prescrevendo a edicao do campo `path` da entrada antiga — ele nao adivinha o par. `MORPH_STANDARDS_BASE` e o unico seam de teste do script e nada em producao o define |
125
- | `framework/hooks/claude-code/**` | hooks-installer.js MORPH_HOOKS array, hook follows fail-open pattern. O `timeout` de cada entrada é em SEGUNDOS, a unidade que o Claude Code lê — em milissegundos ele passa do default de 600 e prende a sessão por horas (canário em `test/hooks/hooks-installer.test.js`); binding `async: true` não tem timeout aplicado |
126
- | `framework/hooks/claude-code/core/denylist.json` (`patterns` e `executableArgs`) | `pre-bash-denylist.js` lê os dois blocos: `patterns` por substring; `executableArgs` por TOKEN (`tokenize` de `framework/hooks/shared/command-tokens.js`) — o executável normalizado (caminho, caixa e `.exe` fora) com um dos `args` depois dele. Com `flags` (grupos, caixa respeitada, `-rf` conta), cada grupo também tem de aparecer no mesmo comando: é o que bloqueia `rm -rf /` e `rm -fr ~` e deixa passar `rm -rf /var/lib/apt/lists/*` (o `args` é token inteiro, nunca prefixo). É o que bloqueia `taskkill /IM` (com `/F` antes, `.exe` ou caminho completo) e deixa passar `taskkill /PID n` e a menção entre aspas numa mensagem de commit (argumento citado é um token só, nunca o executável). A regra é DADO de propósito: o canário de `test/commands/doctor-procs.test.js` reprova `taskkill`/`/IM` em todo código de `src/` e dos hooks, e o corpo da persona só passa pela exceção nomeada da frase de proibição. Regra nova entra em `test/hooks/pre-bash-denylist.test.js` ao lado de uma forma parecida que NÃO pode ser bloqueada |
127
- | `framework/hooks/shared/telemetry-logger.js` (esquema v2 do `events.jsonl`) | os leitores do MESMO evento: `telemetry-reader.js` (`ingestedBytes`), `src/lib/telemetry/metrics.js` (`cost --metrics`), `verify-event.js`, `report-check.js`, os hooks que gravam e o `activity-scanner.js` do dashboard (lê por `loadTelemetryAllTrees` e soma só por `aggregateCost` — nunca uma segunda soma). `isFailedToolCall` é a régua única de "a chamada falhou: não leu nem mudou nada" — um segundo teste de `PostToolUseFailure` volta a contar os bytes de um Read que falhou e a edição que não entrou. Evento v1 (sem `schemaVersion`) nunca vira zero: cai em "sem agente", `unknown-*` ou "sem duração". O esquema mora em `framework/docs/CLI.md` (`cost --metrics`); campo novo aqui entra lá. Os bindings que só registram (`PostToolUseFailure`, `PostToolBatch`, `SubagentStop`) são `async: true` em `hooks-installer.js`; hook que decide nunca é async (canário em `test/hooks/hooks-installer.test.js`) |
128
- | `framework/hooks/shared/command-tokens.js` (`producerOf`, `exitCodeMasked`) | `telemetry-log.js` (grava `exitMasked`), `metrics.js` (o rerun agrupa pelo comando produtor e manda o exit mascarado para `unknown-*`) e os guards de Bash que leem `tokenize`. A tabela de `test/hooks/command-tokens.test.js` é o contrato: cada forma que a cláusula 3 do dispatch manda evitar (`\| tail`, `; echo`, `\|\| true`, `&` de fundo) tem linha lá, ao lado de uma parecida que não mascara (`cd src; dotnet test`, `> log 2>&1`) |
129
- | `framework/hooks/shared/redact-secrets.js` | `telemetry-logger.js` (redige os campos de texto livre, `FREE_TEXT_FIELDS`) e a lista de formas redigidas em `framework/docs/CLI.md`. Regra nova entra em `test/hooks/redact-secrets.test.js` com um caso parecido que NÃO pode ser redigido (`docker run -u 1000:1000`, "Bearer authentication", `git push origin HEAD:refs/heads/main`): regra frouxa apaga o comando que a métrica de rerun lê |
130
- | `framework/skills/core/**` | a linha `**Skills core**` do MORPH.md §7 (canário `test/framework/morph-skills-table.test.js` e o hook local `morph-md-sync-guard`, que leem a MESMA linha: toda skill `morph-*` do disco nela, e nenhuma fantasma), skills-installer.js, SKILL.md frontmatter (a `description` carrega os gatilhos — é ela que o Claude Code mostra) |
131
- | `framework/phases.json` | output-schema.js (phase mappings), test/framework/phases.test.js |
132
- | `framework/templates/**` | REGISTRY.json (template registered), valid Handlebars helpers |
133
- | `framework/templates/REGISTRY.json` (o campo `kind`) | os QUATRO checks de `scripts/validate-framework.js` que leem `kind`/`path`: **4** (resolve arquivo × diretório), **9** (shape exigido por kind), **10** (só lê `path` que é ARQUIVO — `readFileSync` num diretório derruba o validador inteiro com EISDIR, não vira erro de validação) e **12** (cobertura por prefixo; projeto registrado sem arquivo rastreado é erro). `test/framework/validate-framework-registry.test.js` cobra os quatro |
134
- | `framework/ai-pin.json` | `scripts/generate-ai-pin-props.js` (gera `Directory.Packages.props` e `global.json` do ai-kit; `--check` roda dentro de `npm run generate:check`), `src/lib/standards/verified-against.js` (idade contra o pin), `framework/schemas/ai-pin.schema.json`, `test/framework/ai-pin.test.js`. Subir uma versao aqui sem regenerar deixa `npm run verify` vermelho -- e esse e o ponto |
135
- | `framework/templates/dotnet/ai-kit/**` (o projeto .NET) | `REGISTRY.json` (a entrada `ai-kit`, `kind: "project"` — sem ela o Check 12 cobra arquivo por arquivo; com ela apontando para diretorio vazio/inexistente o validate tambem reprova), o **`.gitignore` ANINHADO em `ai-kit/`** (unico jeito de manter `bin/`/`obj/` fora do tarball: `package.json` `files` lista `framework/`, e o npm NAO deixa o `.npmignore` da RAIZ excluir nada sob um diretorio de `files` — medido, 220 arquivos vazavam), `Directory.Packages.props` + `global.json` (GERADOS de `framework/ai-pin.json`, nunca editados a mao) e o job `ai-kit` do CI (T15). O `global.json` carrega `test.runner` porque no SDK .NET 10 `dotnet test` recusa um projeto Microsoft.Testing.Platform (xunit.v3 4.x) sem esse opt-in, e global.json e o unico arquivo de onde o SDK o le. Mexeu em `PackageReference` do `Morph.AiKit.csproj`? `test/framework/ai-kit-package-coverage.test.js` exige que cada pacote esteja OU exercitado por um `using` real em `src/` OU listado na constante `RESTORE_ONLY` do teste (contrato de versao, regua = restore/NU1102) — e a mesma classificacao aparece na tabela de garantias do `README.md` do kit e na Errata E10 do `spec.md`. Desde `src/Morph.AiKit/Media/`, so **um** dos cinco pacotes do pin (`ModelContextProtocol`) segue sem cobertura de compilacao: `OpenAI` e `Google.GenAI` subiram para compilacao quando os adaptadores de midia passaram a usa-los (`using OpenAI.Images;` / `using Google.GenAI.Types;`), e o proprio canario apontou a mudanca ficando vermelho sobre `RESTORE_ONLY`. Mexer no `Media/` que remova esses `using` derruba a garantia de volta |
136
- | `.github/workflows/*.yml` (o job `ai-kit`) | os DOIS workflows carregam o MESMO job — `ci.yml` (toda PR) e `release.yml`, onde `publish` declara `needs: [test, ai-kit]` e continua **sem matrix** (matrix no job que publica roda `npm publish` 1x por OS). Tres detalhes sao load-bearing: `working-directory: framework/templates/dotnet/ai-kit` (o `dotnet test` resolve o `global.json` pelo CWD, nunca pelo `.sln` do argumento — da raiz ele FALHA, enquanto o `dotnet build` funciona, que e o que torna a armadilha facil), `global-json-file` em vez de `dotnet-version` (a banda do SDK vem do pin, nunca digitada no YAML) e o passo `generate-ai-pin-props.js --check` antes do restore (sem ele o job compila props dessincronizadas do pin e reporta verde). `test/framework/ai-kit-ci-packaging.test.js` cobra a forma do job E mede o tarball |
137
- | `src/lib/standards/verified-against.js` | os leitores do MESMO fato "quao velho e este standard": `scripts/check-freshness.js` (hard fail por `Last-verified`), `test/framework/standard-header.test.js` (campos obrigatorios no header) e `framework/ai-pin.json` (a data de referencia). Uma segunda copia do regex e como o contador de `// VERIFY:` ficou inerte: `/^\s*\/\/\s*VERIFY:/` exigia inicio de linha e dois-pontos e enxergava 0 de 10 marcadores reais. **`deriveProvedBy` mora aqui pela mesma razao**, e o quarto leitor e `scripts/generate-standards-registry.js`, que DERIVA `verifiedAgainst`/`provedBy` a cada run — **TRES** tipografias da clausula sao reais no acervo e a regua e a SOMA de paths (27 em 16 entradas), nunca a contagem de entradas: a crase abre DEPOIS de "provado por", ANTES dela, e a clausula tambem aparece em negrito no MEIO da frase (`**provado por**`). Ignorar a segunda perdia 4 das 16 provas; ignorar a terceira perdia 2 paths de `llm-runtime-defaults` **sem mover contador nenhum** — a entrada seguia entre as 16 e o exit era 0. Por isso `test/framework/standards-registry.test.js` pina os paths ESCRITOS A MAO, e nunca chama `deriveProvedBy` para gerar a expectativa do acervo real |
138
- | `src/lib/standards/registry.js` (`loadStandardsRegistry`) | O carregador ÚNICO do registro: lê `STANDARDS.json` e junta por id os campos do sidecar (`META_FIELDS`), devolvendo `{ version, standards, sharedAliases }`. Sidecar ausente ou ilegível = sem aliases (busca pior, nunca quebra); `STANDARDS.json` ausente lança. Leitores: `src/commands/project/standards.js` (`--find` pontua aliases), `src/dashboard/scanner/standards-scanner.js`, `scripts/validate-framework.js` (Checks 1, 2 e 3 sobre `standards`; Check 11 sobre `aliases` + `sharedAliases`) e os testes (`test/lib/standards/registry.test.js`, `test/framework/standards-registry.test.js`, `test/framework/dotnet-testing-standard.test.js`). Um `JSON.parse` direto do `STANDARDS.json` num leitor novo enxerga o registro sem aliases, e o Check 11 lido assim não valida nada. `STANDARDS_FILE`/`STANDARDS_META_FILE` são a grafia única dos nomes, que o gerador importa |
139
- | `src/lib/standards/sections.js` (`ghSlug`, `markdownHeadings`, `sectionBySlug`) | A resolução ÚNICA de heading → slug → seção, com os dois lados que precisam concordar: o gerador (`headingSlugs` valida cada âncora do registro) e o `standards --show <id>#<âncora>` (`resolveStandardSection` em `standards.js`: chave de `anchors` ou slug de heading, seção até o próximo heading de nível igual ou maior, cercas de código ignoradas, slug repetido com sufixo `-n`). Um slugger diferente num lado deixa o gerador aprovar uma âncora que o `--show` não acha. Canário: `test/lib/standards/sections.test.js` |
140
- | `src/core/paths/output-schema.js`| Run `npm run generate` to update phase-utils.js |
141
- | `src/lib/specs/adr-index.js` (o índice de ADRs do projeto) | lê o `2-plan/plan.md` da feature que fecha **pelo `parsePlan`** (`src/lib/plan/plan-parser.js`, a régua única do plano — o ADR leva `body` para o arquivo do índice) e as revogações pela tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` (`revocations.js`). Leitores: `archive.js` (dentro do commit do archive, fail-open), `finish.js` (`adrIndexNote` → `adrIndex`/`revocationProblems` no payload) e `plan-check.js` (`revocationWarnings`, enquanto o plano ainda muda — ele congela no gate do plano). Um arquivo por ADR (`{feature}--ADR-{nnn}.md`) para features paralelas não se tocarem no merge. A tabela de revogação que o template `framework/templates/docs/plan.md` ensina (cercada, dentro do comentário) passa pelos parsers reais em `test/framework/adr-revocation-docs.test.js` |
142
- | `src/lib/plan/plan-parser.js` (`parsePlan`) + `src/lib/plan/checks.js` (o `plan-check`) | O parser ÚNICO do `plan.md` (forma: `framework/templates/docs/plan.md`; `SECTIONS`, `PROOF_KINDS`, `REUSE_VERDICTS`, `ADR_SCOPES`) e a validação/extração ÚNICA (`validatePlan`, `buildChecks`, `planHash` sem a `## Errata` de `framework/hooks/shared/errata.js`, `parallelGroups`, `MAX_SLICE_FILES`/`MAX_SLICES`/`MAX_PARALLEL`). Leitores do parser: `plan-check.js`, `validate-feature.js` (a MESMA `validatePlan` com `knownPersonas`) e `adr-index.js`. O `checks.json`, que só o `plan-check` escreve (o `protect-spec-files` recusa à mão), é lido por `readPlanChecks` — `gate-check.js` (`planChecks` → `planChecksPause` de `risk-detector.js`, que também lê `MAX_SLICES`), `report-check.js`, `reuse-validator.js` e `traceability-check.js` — e, direto do arquivo, por `slice-progress.js` (e por ele `fleet-scanner.js`), `handoff-generator.js`, o `features-scanner.js` do dashboard e o `statusline.py`. Campo novo no `checks.json` sobe `CHECKS_VERSION`; uma segunda leitura do `plan.md` (regex própria numa skill ou hook) é como o plano aprovado e o checado passam a discordar. Canários: `test/lib/plan/{plan-parser,checks}.test.js`, `test/commands/validation/plan-check.test.js` |
143
- | `framework/MORPH.md` / `framework/CLAUDE.md` | All core skills (cross-references) e os **tetos de 20 000 e 4 000 chars** (`test/framework/resident-size.test.js`). Os dois vivem perto do teto: linha nova só entra liberando pelo menos o mesmo número de caracteres **na mesma seção**, com o antes/depois medido — `MORPH_MD_LIMIT`/`CLAUDE_MD_LIMIT` não sobem. Mecânica que um comando decide (sinais de gate, vereditos do `report-check`, roteador por palavra-chave, `approvedBy`) mora na saída do comando e na sua seção `###` do `framework/docs/CLI.md`, nunca aqui. Os títulos `## 3.`…`## 7.`, `### 2.2` e `### Protocolo de …` são lidos por canários de prosa (`worktree-*-prose`, `report-check-wiring`, `plan-contract-freshness`) |
144
- | a divisão sub-agent × suíte (o sub-agent roda só o filtro dos próprios testes; `verify {f}` roda a suíte inteira, sem filtro, uma vez por feature e no Gate 3/CI) | os textos que a declaram e não podem discordar: `framework/docs/CLI.md` (seção "Suítes de teste" — a tabela canônica e a forma de `project.tests[]`), `morph-apply` e `morph-review` (o Gate 3 roda **sem** filtro). Uma superfície que ensine filtrar o verify da feature devolve o F30 pela porta da frente; canário: `test/framework/verify-stack-docs-prose.test.js` |
145
- | `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 |
146
- | `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) |
147
- | 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 (e o que ele guarda: archive, `plan.md`, índice de ADRs, `git log` — o teste reprova a nota que ainda cita use cases); medir só a presença do campo aprovaria uma nota vazia |
148
- | `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" |
149
- | `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`) |
150
- | `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 |
151
- | `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 |
152
- | `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, um relatório de fatia) 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). `treeFingerprint` (`tree-fingerprint.js`) segue a MESMA régua (árvore commitada sem `.morph/`, `config.json` sem `expectedCount`) para `repeatOf` e para o `--ci` sem CI reaproveitar o stamp verde fresco; mudar uma sem a outra faz o reaproveitamento e o gate discordarem. 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` |
153
- | `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 |
154
- | `framework/hooks/shared/slice-progress.js` (progresso e relatório de fatia) | A régua ÚNICA de "em que pé está a fatia" (`sliceProgress`: relatório ausente = pendente, `PARTIAL_MARKER` na 1ª linha = parcial, senão feito) e de "o relatório está completo" (`inspectReport`/`REPORT_MARKERS`: prosa, `**Arquivos:**`, cada `C{n}` de `cobre:`). Leitores: `report-check.js` (reexporta), o Stop hook `validate-completion.js` (`auditSliceReports`), `worktree-context.js`, `handoff-generator.js`/`handoff.js`, `fleet-scanner.js` (pelo `git show` da branch da feature), `status.js` e o `features-scanner.js` do dashboard — nenhum reinterpreta relatório nem tem status próprio de fatia. O `statusline.py` é Python e carrega cópia do marcador: o canário de paridade em `test/hooks/statusline.test.js` a compara com a de JS. O exemplo de relatório da `morph-apply` passa pelos `REPORT_MARKERS` (`test/framework/slice-report-format-rule.test.js`), o Stop hook diz o que falta em cada marca (`MISSING_HINT`), e o do corpo da persona pelo `inspectReport` (`test/framework/morph-apply-skill.test.js`) |
155
- | `src/lib/git/tree-state.js` (o bloco `tree` do `report-check`) | `report-check.js` (toda saída, inclusive o batch `report-check <f> F1 F2`, e a foto `--before-eval` em `4-review/tree-before-eval.json`) e as skills que leem os campos: `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 verificador commitou também aparece. Fora de git o bloco é `null`, nunca árvore vazia. `MUTATION_MARKER` tem a mesma grafia que as skills mandam gravar |
156
- | `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`/`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) |
157
- | `src/lib/tasks/test-runner.js` | `verify-runner.js` (o nó `tests` repassa o `--timeout` e lê `timedOut`), `src/commands/project/worktree.js` (as constantes `WORKTREES_DIR`/`LEGACY_WORKTREES_DIR` que a descoberta ignora — duplicar as strings aqui é como o bug volta), `test/lib/test-runner.test.js` (o canário do ignore de worktrees e o do timeout escalado). Um worktree é o checkout de OUTRA feature: varrê-lo dobra o relógio e reporta sobre código fora da árvore avaliada. `stripAnsi` roda antes de TODO extrator de contagem (ponto único em `parseTestCount`) — uma cor SGR no meio do sumário derrotava os regexes ancorados (#110). O extrator `dotnet` lê os DOIS sumários (#115): a linha VSTest e o bloco MTP (cabeçalho `Test run summary:`/`Resumo da execução de teste:` na coluna 0, `total:` indentado), em pt-BR e en-US — UM bloco por invocação de `dotnet test` (uma solução de dois projetos dá um bloco agregado, medido), então somar blocos é somar links `&&`; um terceiro locale é `null` (capturas medidas em `test/fixtures/test-output/`). `isInside` e `canonicalPath` também são lidos por `mutation/dotnet-introspection.js`, pelo registro de projetos e pelo dashboard — um segundo canonicalizador é como dois leitores passam a discordar sobre o mesmo path |
158
- | `src/lib/specs/screen-checklist.js` (checklist de tela, #129) | o sinal mora aqui (`collectScreenChecklist`) e tem QUATRO leitores: `gate-check.js` na `uiux` (`problems` → pausa) e no `review` (`uncovered`: item decidido sem linha na seção `## Checklist de tela` do `verifier.md`, lida por `verifierScreenChecklist` (`review-reports.js`) — última ocorrência; tela toda "não se aplica" não deve nada), e `approve.js`/`advance.js` com assinatura `auto` (`uiuxAutoApprovalBlock`). Política só em `risk-detector.js`: `uiuxChecklistPause` é o predicado ÚNICO do gate-check pausar e do auto recusar — uma segunda regra no advance fez approve e advance discordarem da mesma feature — e `gateDecisions.uiux.pause === false` destrava os dois só para o mockup que a decisão viu: `persistGateDecision` (`gate-decision.js`, o escritor único de `gateDecisions`) grava `mockupsHash` e `uiuxAutoApprovalBlock` o compara com o arquivo atual — os dois leem `mockupsHash` daqui, e um segundo cálculo do hash (outra normalização de BOM/CRLF) faz toda decisão parecer de outro mockup. `--force` não destrava em nenhum dos dois. Na uiux o sinal também leva `prototypes` (`auditPrototypes`: a linha `**Protótipo:**` da tela, o HTML e os prints `-1280`/`-390` ao lado), lido pelo MESMO `uiuxChecklistPause` depois do checklist — o template `ui-mockups.md` e a `morph-uiux` §3 ensinam essa linha. Mockup legado (`## S\d+:` e nenhuma `## Tela:`, o template anterior) é isento nas duas fases com `exempt` = `LEGACY_MOCKUP_EXEMPTION` à vista no `getRiskSignals`; qualquer outro heading é "nenhuma tela declarada", senão trocar o heading pularia o checklist (fixture: `test/fixtures/mockups/legacy-ui-mockups.md`, o template de main). Os rótulos dos itens casam com DOIS textos que o parser não vê, os dois com canário em `test/lib/specs/screen-checklist.test.js`: o template do prompt da `morph-uiux` §3 e `framework/templates/docs/ui-mockups.md`; mais o formato de linha que `framework/evals/review-checklist.md` (seção "Verificador", "Contrato de tela") prescreve. A `Superfície` é validada contra as chaves de `surfaces` da `ui-designer` (`knownSurfaces`) — é a chave que o `morph-plan` §5 (`superfície:` da fatia) usa para somar standards |
159
- | `src/lib/scout/scout-check.js` (o mapa do scout) | os DOIS escritores do mesmo formato e os que mandam rodá-lo: a persona `scout` do `framework/agents.json` (o CONTRATO DE EVIDÊNCIA e seus exemplos `Formato:`), MORPH.md §6 ("Scout"), `morph-plan` §4, `morph-hotfix` §2 (+ `framework/commands/morph-hotfix.md`) e a entrada do `framework/docs/CLI.md`. Persona que ensina uma forma que o checker reprova faz TODO mapa voltar: `test/framework/scout-persona-format.test.js` passa os exemplos da persona pelo checker. Os relaxamentos (linha já citada, bloco cercado logo depois, listagem sob comando de busca, subseção de Inferido, menção entre aspas) têm cada um o caso parecido que continua reprovando em `test/lib/scout/scout-check.test.js`; crase descritiva depois de citação reprova de propósito, é a forma do erro de campo. Raiz = `--root` ou topo da worktree, nunca a raiz primária. Medição de modelo/effort: `docs/specs/scout-evidence/decisions.md` |
160
- | `src/lib/stack/stack-scan.js` (`morph-spec stack-scan`) | Varredura determinística contra `config.project.stack` (`getProjectTags` de `stack-filter.js`; só stacks configurados são julgados), com `SEVERITIES` e regras por stack. O que fica fora vem de leitores únicos — `isTestPath` (`src/lib/graph/sanitize.js`), `WORKTREE_DIR_NAMES` (`src/lib/worktree/dirs.js`) e `PACKAGE_LOCATIONS` (`src/lib/detectors/index.js`) —, nunca listas próprias. `src/commands/project/stack-scan.js` é fino: com `<feature>` grava `2-plan/stack-scan.md` e recusa fora da árvore dona. Leitores do relatório: `morph-plan` (Bloqueante/Alto vira nó da árvore de decisões) e `morph-uiux`; regras e severidades descritas no bullet `stack-scan` de `framework/docs/CLI.md`, que muda junto. Canários: `test/lib/stack/stack-scan.test.js`, `test/commands/stack-scan.test.js` |
161
- | `framework/hooks/shared/spawn-command.js` (`resolveSpawn`) | a regra ÚNICA de "como executar `npx`/`npm`/`pnpm`/`yarn` neste SO", com dois leitores: `defaultExec` de `e2e-check.js` (o spec do e2e, detectado ou `e2e.command`, e o seed do auth) e `heavy run` (`heavy.js`). No win32 o shim `.cmd` sem shell é ENOENT e o Node recusa `.cmd` com `shell: false`: `npx playwright` vira `node node_modules/@playwright/test/cli.js` quando o arquivo existe, o resto vai ao `.cmd` pelo shell. Canário: `test/hooks/spawn-command.test.js` (inclui um `npx --version` real) |
162
- | `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`). |
163
- | `src/lib/mutation/**` (`morph-spec mutate`, a mutação dirigida do Gate 3) | `src/commands/validation/mutate.js` (fina), `bin/morph-spec.js`, o bullet `mutate` de `framework/docs/CLI.md`, a "Prova por mutação" da seção "Verificador" de `framework/evals/review-checklist.md`, a persona `verifier` e o dispatch em `morph-review` §3 (mutação só depois que revisor e segurança voltam), com os canários `test/lib/mutation/*` e `test/commands/mutate.test.js`. O mundo falso `test/helpers/mutation-world.js` reproduz o build INCREMENTAL do MSBuild (sem essa propriedade o teste da armadilha do mtime não morde). Invariantes: **no máximo 3 mutantes, cada um com `expect`** (`manifest.js` recusa com exit 2 — mutante cujo efeito o verificador não prevê tende a ser equivalente); **com subconjunto utilizável, ele decide** (sobrevivente fica `survived` com escopo `subset`, sem escalar à suíte inteira — o subconjunto é escolhido pela ferramenta, nunca pelo verificador); sem subconjunto, a suíte inteira roda e a morte tem de se repetir; restaurar é **escrita nova**, nunca `os.utime`; `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` é a única grafia de "dotnet build" e `csharpClasses` (`mutation/dotnet-introspection.js`, que só LÊ: dono de arquivo, modo de teste do SDK e tipos de um `.cs`) a única leitura de classe de teste; o filtro do subconjunto é composto por `withFilter`, que soma (`&`) a um `--filter` já declarado. **A varredura C# falha para o lado SEGURO:** toda palavra-chave de tipo do texto CRU tem de ser uma declaração nomeada, senão o arquivo é `scannable: false` e fica fora do subconjunto; 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 de `test/lib/mutation/dotnet-introspection.test.js` ainda morde. Dois parsers de TRX, ou um segundo "dotnet build" digitado, divergem |
164
- | `--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) |
165
- | `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` |
166
- | `src/lib/ci/**` (`verify {f} --ci`, a prova do Gate 3 no CI) | O CI é **outra fonte do MESMO stamp**, nunca uma regra de gate: `verify.js` grava o `verify --json` que o run publicou (artefato `morph-verify`, `CI_RESULT_FILE`) pelo mesmo caminho do local, com `source: 'ci'`, `ciRun` e `sha` = `headSha` do run — `gate-check`/`risk-detector`/`approve` não mudam. Só o ARTEFATO vira prova, nunca a `conclusion` do run (workflow de lint verde não compra o Gate 3), e `findCiWorkflow` só aceita workflow que chama `morph-spec verify` E publica `morph-verify.json` — o `renderCiWorkflow` (`workflow-scaffold.js`) tem de continuar passando nele (canário em `test/lib/ci/workflow-scaffold.test.js`). O run vale por ÁRVORE, não por SHA (`treeMovedSince`): commit só de `.morph/` não invalida, e o gatilho do workflow exclui `.morph/**` menos o `config.json` pela mesma razão (senão `cancel-in-progress` cancela o run do código). `cancelled`/`skipped`/`startup_failure` não são prova. Todo "não dá" é `mode: 'local'` com razão (fallback, nunca pausa); código fora de commit é exit 2; run pendente no prazo é exit 3 sem stamp. A URL do origin é lida CRUA (`remote.origin.url`): `git remote get-url` aplica `insteadOf`, que é como os testes apontam o push para um bare local. Canários: `test/commands/verify-ci.test.js` (stub `test/helpers/gh-ci-stub.js`), `test/lib/ci/*` |
167
- | `nodeStatuses()` em `src/commands/validation/verify.js` | os DOIS blocos `nodes` de `framework/schemas/feature.schema.json`, todos `additionalProperties: false` — `verification.nodes` e `verifications.feature.nodes`. A função é escritora ÚNICA dos dois: um campo novo aqui sem os dois 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` |
168
- | `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 |
169
- | `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/` |
170
- | `framework/hooks/shared/graph-refresh.js` (o refresh em background) | os dois disparos (`graph-refresh-kick`, depois de Bash/PowerShell com `git`/`gh`/`morph-spec`, e o SessionStart de `worktree-context.js`), `graph build\|refresh` (o MESMO lock por repositório) e `worktree.js` (`seedGraphFromRoot`: a worktree nasce com o grafo da raiz e o registro dela — em dia no HEAD só se a raiz estava). **Só a raiz**: o worker destacado nasce com `cwd` no git common dir e nunca recebe o caminho de uma worktree — no win32 um diretório em uso não se apaga, e o `finish` de uma worktree com refresh correndo falhava no meio. A worktree anda por `morph-spec graph refresh`, em primeiro plano. `runGraphify` roda o graphify com `cwd` no git common dir, nunca na árvore (ele lê o alvo, o ignore e o git pelo argumento — medido no 0.9.50). O disparo lê o HEAD pelo disco (`readPrimaryHead`, `git-paths.js`) e não solta processo quando o grafo não pede refresh: ele roda a cada comando git do agente. O step 18b de `setup-infra.js` não instala o hook git do graphify: ele não roda em worktree, não tem `post-merge` e carimba como em dia um grafo sem os arquivos do merge |
171
- | `framework/hooks/shared/git-exclude.js` | ÚNICO escritor dos blocos do morph-spec no `info/exclude` do COMMON dir — o `.git/worktrees/<id>/info/exclude` o git não lê para worktree vinculada. Leitores que passam o seu marcador: `worktree.js`, `handoff-generator.js` e `graph-refresh.js`. Resolve o common dir pelo disco, sem subprocesso: um `git` gerado por um processo destacado abria janela de console no win32 |
172
- | `src/lib/validators/reuse/**` | `validation-runner.js` (case + push universal), `test/fixtures/graph/prospectpro-reuse.graph.json` (recorte do grafo real — regerar com `build-reuse-fixture.mjs`, nunca editar à mão) |
173
- | `src/lib/validators/code-budget/**` | `validation-runner.js` (case + push universal fora do `light`), `framework/docs/CLI.md` (o que conta como teste e os limites 1,5×/40 linhas) e `test/integration/code-budget-verify.test.js`. Lê `addedLinesAgainst` (`default-branch.js`), `isTestFile` (`symbol-scanner.js`, a mesma régua de teste do `reuse`), `isProjectSource`/`isGenerated` (`hooks/shared/source-files.js`) e `commentLines`/`citesProcess` (`hooks/shared/comment-scanner.js`, sobre `stripComments` com `js`). Sempre `warning`. Não conseguir medir é skip `unavailable` com razão, e o `validation-runner` não põe validador pulado em `results`: pulado não conta como verde |
174
- | `framework/hooks/shared/comment-rules.js` / `comment-scanner.js` / `source-files.js` (o hook `comment-guard`) | Os dois leitores do MESMO comentário: o hook `comment-guard` (PostToolUse `Write\|Edit`, bloqueia na hora da edição) e o `code-budget` (avisa no `verify`). Moram em `hooks/shared` porque hook não importa `src/`; o `src/` importa daqui, nunca uma cópia. O hook julga só o comentário ADICIONADO (`tool_response.originalFile`, medido: `null` no Write que cria) e só FORMA — processo, banner/`#region`, bloco > 3 linhas, doc-comment fora de pacote publicado —, nunca "repete o código". É o ÚLTIMO elo da chain `Write\|Edit` (um block encerra a chain; canário em `test/hooks/hooks-installer.test.js`). O vocabulário de processo (`citesProcess`) inclui o id de cenário/fatia do plano (`C{n}`/`F{n}`), `plan.md` e `checks.json`; a tabela de `test/hooks/comment-scanner.test.js` põe cada forma ao lado do vizinho que NÃO pode casar (tecla `F5`/`KeyCode.F1`, `F1 score`, `C4 model`, célula `C3`, `#F5F5F5`, `0xC1`) — e a mensagem do `code-budget` lista o mesmo vocabulário. Marcador que template precisa manter (`READS STANDARD(S):`, `VERIFY:`) entra em `ALLOWED`; o canário de template em `test/hooks/comment-rules.test.js` reprova template que dispararia o hook no scaffold |
175
- | o ciclo de reuso (tabela `## Reuso` do `plan.md`) | `src/lib/plan/plan-parser.js` + `checks.js` (o `plan-check` extrai `reuse` com `why` no CRIAR para o `checks.json`), `morph-plan`, o corpo de dispatch da persona (`DISPATCH_CONTRACT` em `agents-installer.js`, cláusula de reuso), o validador `reuse` (`src/lib/validators/reuse/`: vereditos e arquivos da feature lidos por `readPlanChecks`, nunca do `plan.md` cru) e `test/framework/graph-fallback.test.js`. Canário ponta a ponta: `test/integration/reuse-verify.test.js` (CRIAR declarado no plano rebaixa a duplicata a aviso pelo `verify` real) |
176
- | `framework/mcp/registry.json` | mcp-installer.js, claude-config-detector.js, agents-installer.js (personas' `mcp` field), `scripts/validate-framework.js` Check 8, `test/framework/mcp-registry.test.js` |
177
- | `src/lib/detectors/claude-config-detector.js` (`readEnabledPlugins`) e `src/lib/installers/mcp-installer.js` (`getInstalledMcps`) | os QUATRO chamadores de `orchestrateMcpSetup` (`mcp-setup.js` ×3, `init.js`): chamada sem `enabledPlugins` recria a duplicata `mcp__X` × `mcp__plugin_X_X` (canário de fiação em `test/lib/installers/mcp-installer.test.js`). `readEnabledPlugins` é a leitura ÚNICA de "plugin ligado" — o `doctor --procs` a reusa em `procs/mcp-config.js`; o prefixo `plugin_<nome>_` sai de `serverNames` do registry via `mcpServerNames` |
178
- | `src/lib/procs/**` (`doctor --procs`) | `src/commands/project/doctor-procs.js`, o ramo `--procs` de `doctor.js` (entra DEPOIS do de `--worktrees`, fora das fatias que `test/commands/doctor-ai.test.js` corta), `bin/morph-spec.js` (`--prune` é UMA flag para os dois modos) e `test/commands/doctor-procs.test.js`. **Só `prune.js` mata, e por PID:** o canário varre o grafo estático do comando (`prune.js` só entra por import dinâmico dentro de `if (options.prune)`), reprova escrita, spawn e `process.kill` no caminho de leitura — com exceção nomeada ao `execFileSync` de `list.js` — e reprova `taskkill`/`Stop-Process`/`pkill`/`killall`/`/IM` em qualquer código de `src/` e dos hooks. As proteções de `classify.js` e a checagem de PID reaproveitado de `prune.js` têm teste por mutação sobre processo real: tirar uma delas mata um sleeper de teste. `null` é desconhecido, nunca lista vazia. `MORPH_PROCS_FIXTURE` é o único seam de teste; nada em produção o define |
179
- | `src/lib/heavy/**` e o núcleo `framework/hooks/shared/heavy-{config,holders,slots,command}.js` (semáforo de nós pesados) + `process-alive.js` | `verify-runner.js` (`runVerify` é só o wrapper que cria UM `createHeavyGate` e o libera no `finally`; `gate.enter` é lazy e roda imediatamente antes do primeiro spawn real nos QUATRO pontos — build, testes, `--linux` e o e2e por `beforeHeavy` em `framework/hooks/shared/e2e-check.js`. Nó novo que spawna processo pesado sem `enter` fura o semáforo: `test/lib/verify-runner-heavy.test.js`), `verify.js`/`e2e.js` (`HeavyBusyError` → exit 3, **sem stamp e sem evento `Verify`**: fila não é gate; `nodeStatuses`/`PARTIAL_RUN_FLAGS` não ganham `busy`), `heavy.js` (`heavy status|run`), `cleanup.js` (`afterHeavyRun` recusa com `!enabled` — sem semáforo nada prova que outra sessão não compila —, fora de `feature|e2e-down|prune`, com holder vivo e com `dotnet build|test` vivo, e roda o `build-server shutdown` sob `runExclusive`), `doctor-procs.js` (holders são protegidos da poda; `afterHeavyRun` no `--prune`), `framework/schemas/config.schema.json` (bloco `heavy`; canário schema × `HEAVY_DEFAULTS` em `test/meta/heavy-isolation.test.js`) e `scripts/run-tests.mjs` (`HEAVY_OFF`: sem `MORPH_HEAVY=off` no env de todo filho a suíte enfileira nos slots REAIS de quem a roda). **O núcleo mora em `hooks/shared`** (hook não importa `src/`; `src/lib/heavy/` guarda só o `gate`, o `cleanup` e o comando): `heavy-holders.js` só lê (é o que o `doctor --procs` alcança no caminho somente leitura) e `heavy-slots.js` escreve; `graph-refresh.js` importa `pidAlive` em vez de ter cópia. **`heavy.gateBash` (opt-in):** `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`: um comando que um guard bloqueia não segura vaga) e `heavy-release` (`PostToolUse` e `PostToolUseFailure`, esta continua `async`) tomam e devolvem a vaga por `tool_use_id`. **A vaga de lease não tem pid** (`pid: null`, `leaseId`, `ttlMs`): `readLockFile` a vence por TTL, nunca por `pidAlive` — o processo do hook já saiu —, e uma chamada recusada por regra ou interrompida, que não dispara `Post*`, segura a vaga até o TTL. A espera do gate é `min(waitTimeoutSec, 540)`: um `PreToolUse` que estoura o timeout de 600 s deixa a chamada rodar, então a recusa tem de sair dali. **Nunca reescreva o comando para pôr slot:** o `updatedInput` é julgado contra as regras do usuário na forma reescrita. `heavyKind` (`heavy-command.js`) decide pelo executável de cada segmento fora de aspas; forma nova entra na tabela de `test/hooks/heavy-command.test.js` ao lado de um caso leve parecido. Os imports do semáforo em `heavy-gate`/`heavy-release` são **lazy** de propósito (os elos rodam em todo Bash e quase nenhum é pesado). Canários: `test/hooks/heavy-gate.test.js` (medido por mutação, 17 de 17), `heavy-slots`/`heavy-holders`/`heavy-command`, e os pins de ordem e `async` em `test/hooks/hooks-installer.test.js`. Slot vago = pid morto OU idade > `maxHoldMinutes`; `null`/diretório não criável é fail-open (não fabrica fila). `tearDownStack` (`e2e-check.js`) usa `--remove-orphans`: `test/lib/e2e/stack-teardown.test.js` pina o argv |
180
- | `src/lib/worktree/agent-reaper.js` (reaper de sub-agent) + `framework/hooks/shared/agent-reap.js` | `worktree.js` (`runWorktreeRemove` é o núcleo sem `process.exit` que o reaper chama e `worktreeRemoveCommand` só imprime e sai; `teardownWorktree({ relinkOnFailure })` é opt-in do reaper — o `worktree remove` manual segue deixando a infra desmontada em falha, de propósito), `worktree-audit.js` (`agent-worktree-leftover`; a árvore `agent-<hex>` sai do ramo `morph-task` antigo para não duplicar o `unintegrated-work`), `doctor.js` (`--prune` roda o reaper), `worktree-context.js` (`kickAgentReap` no SessionStart: worker destacado, decisão só pelo disco, throttle de 10 min) e `bin/morph-spec.js`. **Provas — todas, senão preserva, e `null` preserva:** o último evento do `events.jsonl` da árvore é o `AgentUsage` do próprio `agentId`; graça; árvore limpa (`--untracked-files=all` filtrado por `withoutLeftoverIgnore`); zero commit exclusivo por `rev-list --stdin` **sem `--all`** e com os `morph-task/agent-*` fora do conjunto de refs; não é âncora de sessão. **O lock por repositório é obrigatório:** dois reapers concorrentes deixam um relink recriar junction no meio da remoção do outro, e o `--force` as atravessa. Canários: `test/lib/worktree/agent-reaper.test.js` (cada prova sozinha vira o veredito; medido por mutação), `test/commands/doctor-agent-reap.test.js`, `test/hooks/agent-reap.test.js`, `test/framework/worktree-reap-prose.test.js`. O `events.jsonl` da árvore removida vai para `finish-backup/task/` (`.morph/` é shieldable no `ignored-triage`) e some do `cost --all-trees` |
181
- | `src/lib/worktree/vscode-settings.js` (`.vscode/settings.json`) | os SEIS pontos que o chamam: `init`/`update` (`update.js` pelo `fx`, e `setup-infra.js` step 18a — `init` delega ao `update`, a 2ª chamada sai `unchanged`), `runWorktreeSetup`/`runWorktreeProvision` (`rootAndEditorReport`, fail-open) e `doctor --worktrees --prune`. `planEditorSettings` só lê e `applyEditorSettings` escreve por `fx.writeFile`: o `update --dry-run` só é honesto porque o passo emite nos dois modos (canário de igualdade dry × real em `test/commands/update-dry-run.test.js`). **Só adiciona chave que falta**, nunca altera valor do usuário; comentário, vírgula sobrando, JSON ilegível, raiz não-objeto ou arquivo **rastreado** → não escreve e devolve o `snippet`. O arquivo **criado** vai para o `info/exclude` do common dir (`git-exclude.js`) — sem isso ele é sujeira da raiz e o `finish --merge` recusa. `bin/obj` em `files.exclude` só com stack .NET. `git.detectWorktrees` **não** é escrito de propósito (recurso de listagem, sobrepõe preferência global). `worktree.editorSettings` do `config.schema.json` e `MORPH_EDITOR_SETTINGS=0` desligam. Canários: `test/lib/worktree/vscode-settings.test.js` (medido por mutação, 11 de 11), `test/commands/editor-settings-wiring.test.js` |
182
- | `framework/hooks/shared/worktree-root.js` (`worktree.root`) | o resolvedor ÚNICO de "onde as worktrees moram" e de "está no contêiner" — a UNIÃO do layout configurado com `worktrees/` e `.worktrees/`, para a árvore antiga seguir reconhecida. Todo leitor precisa dele, e um que volte ao literal fica cego para a externa ou acusa `out-of-container` em árvore boa: `worktree.js` (`buildWorktreePath`, mkdir do pai, `findWorktreeForFeature`), `worktree-audit.js` (`inContainer` e `dirsOnDisk`, que só varre `<root>/<repo>-<hash>/`), `agent-reaper.js`, `worktree-context.js` (`isOutsideContainer`), `worktree-create.js` (fallback em git puro), `registry.js` (`insideWorktreeContainer` lê o `.git` da árvore) e os scanners do dashboard (`features-scanner`, `project-api`). `src/lib/worktree/dirs.js` só reexporta as constantes daqui. **Root dentro da árvore é recusado com aviso:** as listas orientadas pelo nome `worktrees/` (`.gitignore`, `graphifyignore`, `test-runner`, `linux-runner`, `architecture-scanner`, `ai-project-scan`, globs do editor) não o enxergariam. A chave do repositório é `basename-<hash da raiz>`: só o basename colidiria entre repositórios. `buildWorktreePath` preserva a string como veio quando não há root (o teste dele passa `/project`). Canários: `test/hooks/worktree-root.test.js`, `test/commands/worktree-root.test.js` (medido por mutação, 14 de 14) |
183
- | `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` |
184
- | 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) |
185
- | `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` §4, `test/framework/worktree-governance-prose.test.js`, `resident-size.test.js` (MORPH.md perto do teto de 20k). **Â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` |
186
- | `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 |
187
- | `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. `discoverFrontendPackages` (a união do fallback de `resolveTestNodes` e o WARN do `doctor --full`) varre as pastas de `PACKAGE_LOCATIONS`, de `framework/hooks/shared/frontend-locations.js` (re-exportada por `src/lib/detectors/index.js`; o e2e lê `E2E_SPEC_LOCATIONS` = a mesma lista + `web`) — uma segunda lista de pastas é como dois detectores começam a discordar |
188
- | `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` reusa — 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 |
189
- | `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`), a tabela `#### Sinais` sob `### gate-check` do `framework/docs/CLI.md` (a régua escrita do comando; o MORPH.md §5 só lista os sinais que a sessão passa por `--signals` — canário em `test/framework/verify-stack-docs-prose.test.js`). Política escrita no coletor cria duas fontes para o mesmo fato — foi assim que `gate-check` e `approve` divergiram uma vez sobre o Gate 3. Os relatórios do Gate 3 são lidos por UM leitor (`src/lib/review-reports.js`) e decididos por UMA regra (`reviewPause`): `approve` exige cada relatório exigido com `status: ok`, `gate-check` pausa em `FIX`, bloqueador aberto ou sem tipo, ou `Cn` sem linha PROVADO. **O plano entra pelo `checks.json`:** `planChecksPause` é o predicado ÚNICO de "o plano não foi extraído até zero erro" — o `gate-check` pausa o gate do plano por ele e `approve`/`advance` automáticos recusam por ele (`planAutoApprovalBlock`); `sliceCount > MAX_SLICES` pausa. `featureGeneratesMedia` lê os `standards` das fatias por id ou caminho (`standardIdOf`), e o `report-check --before-eval` usa o mesmo predicado para rotear o verificador a opus — canário ponta a ponta em `test/framework/media-claims.test.js`. A prova de verify é sempre da feature inteira: `--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`) |
190
- | `src/lib/review-reports.js` (os relatórios do Gate 3) | O leitor ÚNICO de `4-review/{verifier,reviewer,security}.md`: veredito (o último, append-only), bloqueadores tipados (`BLOCKER_TYPES`), `PARCIAL` (o `PARTIAL_MARKER` de `slice-progress.js`, nunca uma cópia), `**Reviewed-SHA:**` e a `## Checks` do verificador. Leitores: `gate-check.js` (`collectReviewSignal` → `reviewSignal`), `approve.js` (status por papel, `--self-assessed` e o `Reviewed-SHA`, o pior vence), `report-check.js` (`reviews` por papel; `reviewModels` no `--before-eval`), `risk-detector.js` (`reviewPause` lê o sinal e `BLOCKER_TYPES`), `screen-checklist.js` (`verifierScreenChecklist`), o dashboard (`features-scanner.js`, por `reviewReportsFromDir` — sem git, `security` conta só presente) e o hook `state-sync` (que, por não importar `src/`, só testa a existência de `verifier.md`/`reviewer.md`). Quais papéis são exigidos é UMA regra: `requiredReviewRoles` + `securityReviewFor` via `collectReviewRequirement` (`gate-check.js`), lida pelos três comandos — uma segunda noção faz o approve aprovar o que o gate pausa. Cobertura: `checksCoverage` — status por igualdade EXATA (`NÃO PROVADO` contém `PROVADO`), Cn sem linha falta; `checks.json` ilegível pausa. Canários: `test/lib/review-reports.test.js` (medido por mutação: aceitar `NAO PROVADO` como prova e tratar Cn sem linha como coberto derrubam 3 testes cada) e o bloco "relatórios de review no Gate 3" de `test/commands/gate-check.test.js`; o fixture é `test/helpers/review-fixtures.js` |
191
- | `src/lib/installers/effects.js` | os NOVE pontos de emissão que atravessam o caminho de `update`: `update.js` (clean, payloads, spawn de `generate-refs.js`, `gitIndex`), `skills-installer.js`, `agents-installer.js`, `setup-infra.js` (`installRulesForStack`, `installCommandsForStack`, `updateClaudeMdAgentsBlock`), `hooks-installer.js` (`installClaudeHooks`, `installGlobalStatusline`), `file-copier.js` (`updateGitignore`, `ensureGitattributes`), `claude-md-injector.js`, `version-checker.js` (`saveProjectMorphVersion`) e `templates-reconcile.js`. **O caminho real também emite** — o resumo de uma execução real sai de `list()`, e é por isso que um ponto de emissão esquecido some das DUAS saídas ao mesmo tempo. Canários: `test/lib/installers/effects.test.js` (igualdade dry × real do MESMO roteiro) e `test/commands/update-dry-run.test.js` — que tem DOIS canários e precisa dos dois: **igualdade** prova que os dois lados percorrem o mesmo caminho, **diferencial** (mudar `--templates` muda a previsão) prova que o caminho lê a flag. Só igualdade aprova uma previsão e uma execução que ignoram a MESMA flag — foi medido nesta feature, por mutação. Um laço de limpeza de órfão novo tem de nascer calado em `fx.dryRun` (D6): o clean o subsome. E a lista de `frameworkDirsToClean` é pinada **à mão** no teste (`DIRETORIOS_APAGADOS_POR_INTEIRO`, nove caminhos, `deepEqual` ordenado): derivar a expectativa chamando a própria função deixou a suíte inteira cega — medido, dava para remover 2 das 10 entradas de então sem matar nenhum dos 207 arquivos de teste |
192
- | `src/lib/ai/**` (`doctor --ai`) | `src/commands/project/doctor.js` (`doctorAiCommand` + a guarda de uso indevido), `bin/morph-spec.js` (`--ai`), `framework/docs/CLI.md`. **`--ai` NUNCA entra no `doctor --full` nem no default** — o renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` e `test/commands/doctor.test.js` exige zero `✗` neste repositório. **Nada alcançável por `--ai` pode escrever**: `test/commands/doctor-ai.test.js` varre o grafo de imports E o corpo de `doctorAiCommand` (com os imports dinâmicos fixados nos dois permitidos), reprovando `writeFileSync`/`spawn`/`fs-extra` em CÓDIGO — comentários apagados antes, porque os módulos EXPLICAM por escrito que não fazem isso e medir a menção seria medir uma cópia do objeto. `null` é DESCONHECIDO e vira `skip` **com razão obrigatória**; `[]` é "perguntei e não achei" e vira `ok` |
193
- | `src/lib/ai/registry-audit.js` | o leitor de PRODUÇÃO do mesmo documento: `framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs` (`CommentHandling = Skip`, `AllowTrailingCommas`, `AllowedAliasProperties` com `pricing`/`pricingWaiver`) e o exemplar `model-registry.v2.json` que o framework manda COPIAR. A auditoria tem de enxergar a **mesma forma** que o leitor de produção enxerga: com `JSON.parse` estrito, duas das cinco checagens saíam `skip` sobre o exemplar canônico — e `skip` não muda exit code, então o silêncio era total. A tolerância a JSONC vive em `framework/hooks/shared/comment-stripper.js`, ÚNICA (o `code-audit.js` usa o mesmo varredor, e o `code-budget` também, o único com a opção `js` — template e regex literal de TS/JS; sem ela o varredor é o de antes) — e a paridade é nos DOIS sentidos: `jsoncToJson` **lança** no comentário de bloco não fechado, porque o `JsonCommentHandling.Skip` do .NET lança, e ser mais frouxo que o kit é aprovar documento que a app recusa carregar. Ao mascarar strings, **as aspas delimitadoras ficam**: apagá-las junto faz um elemento-string virar espaço puro e o detector de vírgula sobrando comer um SEPARADOR real (`{"tags":["a","b"]}` saía inválido). Canário obrigatório: array terminado em **string**, com e sem vírgula sobrando — uma suíte só com arrays de número passa dos dois lados dessa inversão. A **REGRA DURA** do exemplar — *todo alias declara `pricing` OU `pricingWaiver`, nunca os dois, nunca nenhum* — é do alias, não de um mapa no topo: ler só o mapa dava 6 falsos positivos sobre um documento correto. O canário **copia o exemplar do disco**, nunca uma paráfrase dele |
194
- | `src/lib/worktree/dirs.js` | ÚNICA fonte do nome do container (`worktrees/`, sem ponto — Jest esconde diretório com ponto e reporta 0 testes sem erro). `worktree.js` re-exporta; `graphifyignore.js`, `file-copier.js`, `architecture-scanner.js`, `test-runner.js`, `worktree-audit.js`, os scanners do dashboard (`features-scanner.js`, `project-api.js`), `tasks/linux-runner.js` (exclusões do `tar` do `--linux`) importam. `test/lib/worktree/dirs.test.js` cobra cada leitor |
195
- | `src/dashboard/**` (páginas e scanners) | `test/dashboard/pages-smoke.test.js` renderiza as 7 rotas e os painéis de detalhe pela config Vite do dashboard com os dados REAIS do scanner — build verde não prova nada (8.38.0 saiu com `ReferenceError` na rota inicial). Campo que a página lê tem de sair do scanner: `activePhases` é DERIVADO das linhas de dispatch das skills de fase (`personaPhases`, com `isImplementer`), nunca uma lista à mão. Edge do React Flow só existe no cliente (chave duplicada não aparece no SSR — o dedupe de `standardRefs` mora no scanner). `src/dashboard/.gitignore` é o que mantém `node_modules` fora do tarball (`test/dashboard/packaging.test.js`). `scanner/feature-status.js` (`isFinished`) é a regra ÚNICA de "feature encerrada" do servidor (portfólio, `awaiting`, `sliceIndex`) e do cliente (lista e quadro) — arquivada conta como encerrada seja qual for o `status` legado. O plugin engole o `full-reload` do Vite para `.html` sob `.morph/` (`isMorphDataReload`): `handleHotUpdate` devolvendo `[]` NÃO segura `.html` no Vite 6, e um agente gravando um mockup recarregava o dashboard. `resolveFeatureTree` (git, ~0,3 s por chamada) é lembrado por feature no `project-api.js` e só é esquecido por evento estrutural (`add`/`unlink`/`addDir`/`unlinkDir`) ou TTL de 60 s — um scan síncrono trava o event loop do dev server inteiro |
196
- | `src/lib/projects/registry.js` (projetos da máquina, `~/.morph-spec/projects.json`) | os TRÊS escritores automáticos — `init`, `update` (nunca sob `--dry-run`) e `dashboard` — mais o add/remove do portfólio (`src/dashboard/scanner/portfolio.js`). Registrar é fail-open: registro ilegível ou não gravável nunca derruba o comando. Nunca registra pasta sob o temp do sistema (as suítes rodam `init`/`update` ali às centenas, inclusive por CLI em spawn que herda o home real) nem checkout de worktree (`WORKTREE_DIR_NAMES`). Teste que registra usa `MORPH_PROJECTS_REGISTRY` apontando para um temp, ou o seam `tempRoot` — nenhuma suíte escreve o `~/.morph-spec` de quem roda. Leitura poda projeto cuja pasta sumiu; descoberta é UM nível abaixo das pastas-mãe e só sugere, nunca adiciona |
119
+ A matriz "mexeu em X → verifique Y" mora em `.claude/rules/matrix-*.md`, uma rule por área com `paths:`: o Claude Code a carrega só quando a sessão toca um arquivo daquela área. Linha nova entra na rule da área do arquivo-chave (a coluna da esquerda); área nova é rule nova, com seus `paths:`.
120
+
121
+ | Rule | Área |
122
+ | --- | --- |
123
+ | `.claude/rules/matrix-agents.md` | Personas (`agents.json`) e o installer delas |
124
+ | `.claude/rules/matrix-standards.md` | Standards, registro e seções |
125
+ | `.claude/rules/matrix-hooks.md` | Hooks, telemetria e comment-guard |
126
+ | `.claude/rules/matrix-charter.md` | Carta NLH, skills e docs instaláveis |
127
+ | `.claude/rules/matrix-templates-ai.md` | Fases, templates, ai-kit e CI |
128
+ | `.claude/rules/matrix-plan-review.md` | Plano, relatórios de fatia, gates e Gate 3 |
129
+ | `.claude/rules/matrix-verify.md` | Verify, testes, mutação e CI |
130
+ | `.claude/rules/matrix-worktree-git.md` | Worktrees, finish e git |
131
+ | `.claude/rules/matrix-graph.md` | Grafo de codebase e validador de reuso |
132
+ | `.claude/rules/matrix-platform.md` | MCP, installers, procs, heavy e dashboard |
197
133
 
198
134
  ### Test Coverage
199
135
 
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:** 9.2.0
6
+ **Version:** 9.3.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -380,4 +380,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
380
380
 
381
381
  ---
382
382
 
383
- *morph-spec v9.2.0 by [Polymorphism Tech](https://polymorphism.tech)*
383
+ *morph-spec v9.3.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -46,6 +46,7 @@ const gateCheckCommand = lazy('../src/commands/state/gate-check.js', 'gateCheckC
46
46
  const createCommand = lazy('../src/commands/state/create.js', 'createCommand');
47
47
  const retypeCommand = lazy('../src/commands/state/retype.js', 'retypeCommand');
48
48
  const handoffCommand = lazy('../src/commands/state/handoff.js', 'handoffCommand');
49
+ const nextCommand = lazy('../src/commands/state/next.js', 'nextCommand');
49
50
  const listCommand = lazy('../src/commands/state/list.js', 'listCommand');
50
51
  const deleteCommand = lazy('../src/commands/state/delete.js', 'deleteCommand');
51
52
 
@@ -265,6 +266,12 @@ program
265
266
  .option('--here', 'Force building/writing in the current directory, even if another tree owns this feature')
266
267
  .action((feature, options) => handoffCommand(feature, options));
267
268
 
269
+ program
270
+ .command('next <feature>')
271
+ .description('The next step of the loop, decided from disk (phase, gates, slice reports, verify stamp, fail streak) in the tree that owns the feature. Read-only.')
272
+ .option('--json', 'Output as JSON')
273
+ .action((feature, options) => nextCommand(feature, options));
274
+
268
275
  const graphCommand = program
269
276
  .command('graph')
270
277
  .description('Grafo de codebase (graphify) com saneamento determinístico — fail-open: sem grafo, tudo segue por grep');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "9.2.0",
3
+ "version": "9.3.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",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v9.2.0 by Polymorphism Tech*
188
+ *morph-spec v9.3.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v9.2.0 by Polymorphism Tech*
252
+ *morph-spec v9.3.0 by Polymorphism Tech*
@@ -5,9 +5,8 @@
5
5
  ## Início de cada sessão
6
6
 
7
7
  1. Leia `.morph/framework/MORPH.md` uma vez (de novo após `/compact`): papéis, fases, gates, dispatch.
8
- 2. `.morph/state.json` é índice gitignored, só para achar a feature ativa; o estado autoritativo é `.morph/features/{feature}/feature.json`.
9
- 3. Existe `.morph/features/{feature}/handoff.md`? Leia antes de re-derivar contexto à mão.
10
- 4. Feature em andamento → assuma o papel da fase e continue entre gates sem esperar instrução. Nenhuma ativa → pergunte qual trabalhar.
8
+ 2. O SessionStart injeta a feature ativa e `▶ próximo:`; sem ele, `morph-spec next <f>`. Estado autoritativo: `.morph/features/{f}/feature.json` (`state.json` é índice — nunca edite).
9
+ 3. Feature em andamento → faça o que o `next` manda, entre gates, sem esperar instrução. Nenhuma ativa → pergunte qual trabalhar.
11
10
 
12
11
  ## Regras
13
12
 
@@ -24,6 +23,7 @@ Prefixo `morph-spec`; flags e porquês em `.morph/framework/CLI.md` (um `###` po
24
23
  | Comando | Para quê |
25
24
  |---|---|
26
25
  | `create <f> --description "…" --type <t> [--worktree]` | nasce a feature; exit 2 = você escolhe o tipo |
26
+ | `next <f>` | o próximo passo, decidido do disco |
27
27
  | `stack-scan [f]` | desvios do código contra `project.stack` |
28
28
  | `tests-audit` | candidatos a apagar/fundir na suíte (só lê) |
29
29
  | `plan-check <f>` | valida `plan.md`, grava `checks.json`, imprime as ondas |
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Leia uma vez por sessão (e de novo após `/compact`). Os comandos imprimem a própria mecânica (sinais, vereditos, próximo passo); referência completa em `.morph/framework/CLI.md`.
4
4
  >
5
- > **Depois de ler:** `.morph/state.json` é um índice fino e gitignored, reconstruído a cada load — use-o só para achar a feature ativa e nunca o edite (a edição se perde no próximo load). O estado autoritativo e commitado é `.morph/features/{feature}/feature.json`. Feature `in_progress` → assuma o papel da fase e continue; sem feature ativa → pergunte qual trabalhar.
5
+ > **Depois de ler:** o SessionStart já injeta a feature ativa e a linha `▶ próximo:`; `morph-spec next <f>` decide o passo do disco (fase, gates, relatórios, verify). Estado autoritativo: `.morph/features/{f}/feature.json`; `.morph/state.json` é índice gitignored — nunca o edite. Sem feature ativa → pergunte qual trabalhar.
6
6
 
7
7
  ---
8
8
 
@@ -12,14 +12,11 @@ Entre gates, você opera sozinho: o dev valida, você executa.
12
12
 
13
13
  | Situação | O que você faz |
14
14
  | --- | --- |
15
- | Gate aprovado (plano / uiux / review) | Avança para a próxima fase na hora |
16
- | Fatia relatada na fase implement | Despacha a próxima fatia pronta sem esperar instrução |
15
+ | Entre gates (gate aprovado, fatia relatada, verify verde ou vermelho) | Faz o que `morph-spec next <f>` mandar — `dispatch`, `verify` (uma vez, sem filtro), `fix`, `review`, `finish` — sem esperar instrução nem anunciar |
17
16
  | Ambiguidade de escopo coberta pelo plano | Resolve pelo lado conservador e registra no relatório |
18
17
  | Ambiguidade bloqueante (requisito ausente ou contraditório) | Uma pergunta via `AskUserQuestion` |
19
- | Todas as fatias relatadas | `morph-spec verify <feature>` uma vez, sem filtro, e cada cenário `Cn` com a evidência que o prova |
20
- | Verify verde e cenários provados | Segue para a review, sem anúncio |
21
- | Verify vermelho ou cenário sem evidência | Loop de correção autônomo com o feedback, até 3 tentativas, sem pausa humana |
22
- | 3 tentativas sem fechar | `AskUserQuestion` (aceitar/continuar/pular) — o único ponto de pausa do loop. Nunca uma 4ª tentativa por conta própria nem re-dispatch: sem informação nova, ela repete o erro |
18
+ | Cenário `Cn` sem evidência | Loop de correção com o feedback, como o verify vermelho, antes da review |
19
+ | `next` devolve `escalate` (3 verifies vermelhos seguidos) | `AskUserQuestion` (aceitar/continuar/parar) — o único ponto de pausa do loop. Nunca uma 4ª tentativa por conta própria nem re-dispatch: sem informação nova, ela repete o erro |
23
20
  | `verify` exit 3 (fila pesada cheia) | A fila já esperou: no máximo uma chamada nova, depois avise. Nunca laço de espera |
24
21
 
25
22
  **Regra de ouro:** dúvida que não bloqueia → resolva e registre; dúvida que bloqueia → uma pergunta, no máximo. Vale **depois** do gate do plano: na abertura, o **modo entrevista** (`morph-plan` §3) pergunta até fechar o entendimento, e esse é o comportamento desejado.
@@ -127,7 +124,7 @@ Features independentes rodam em paralelo, uma por worktree e uma instância de C
127
124
 
128
125
  ### Implement (`3-implement/`)
129
126
 
130
- - `3-implement/reports/F{n}.md` — um por fatia, do sub-agent ou do orquestrador quando inline; ausente = pendente, `PARCIAL` na 1ª linha = parcial. São o registro da implementação (`**Arquivos:**`, `**Standards:**`; não há recap). Formato em `morph-apply`.
127
+ - `3-implement/reports/F{n}.md` — um por fatia (sub-agent, ou você se inline); ausente = pendente, 1ª linha `PARCIAL` = parcial, `BLOQUEADO` = decisão sua. Registro da implementação (`**Arquivos:**`, `**Standards:**`; sem recap); formato em `morph-apply`.
131
128
  - `3-implement/evidencias/{Cn}.md` — no cenário `prova: eval` (entrada, modelo, saída, veredito) ou `prova: manual` (passo e resultado).
132
129
 
133
130
  ### Review (`4-review/`)
@@ -43,7 +43,8 @@
43
43
  ],
44
44
  "mcp": [
45
45
  "context7",
46
- "graphify"
46
+ "graphify",
47
+ "microsoft-learn"
47
48
  ],
48
49
  "model": "sonnet",
49
50
  "maxTurns": 120,
@@ -83,7 +84,8 @@
83
84
  "mcp": [
84
85
  "context7",
85
86
  "graphify",
86
- "neon"
87
+ "neon",
88
+ "microsoft-learn"
87
89
  ],
88
90
  "model": "sonnet",
89
91
  "maxTurns": 120,
@@ -144,7 +146,8 @@
144
146
  ],
145
147
  "mcp": [
146
148
  "context7",
147
- "graphify"
149
+ "graphify",
150
+ "microsoft-learn"
148
151
  ],
149
152
  "model": "sonnet",
150
153
  "maxTurns": 120,
@@ -365,7 +368,7 @@
365
368
  ],
366
369
  "model": "sonnet",
367
370
  "maxTurns": 90,
368
- "persona": "Você é o verificador independente do Gate 3. Não participou da implementação. Sua missão é PROVAR cada cenário `C{n}` do `2-plan/checks.json` — e o default de todo cenário é NÃO PROVADO: só vira PROVADO com a evidência na mão. Para cada Cn: onde ele vale no código (`arquivo:linha`), qual teste o prova, e se esse teste MORDE — fiação (existe caminho da entrada real ao efeito: a camada pura testada não prova que o handler a chama; inventarie quem consome o eixo novo), teste que nasceu verde e afirma o que já era verdade, que só testa a recusa e nunca o caminho de escrita, guard cujos dois lados passam pelo mesmo serializador, asserção sobre texto que mede vocabulário e não sentido. Verificações mecânicas (.NET minimal API / Next): todo serviço injetado numa lambda de endpoint tem registro em `Program.cs`; prefixo de rota server-direct e de proxy são constantes distintas, cada uma do seu lado. Com o stack no ar (URL no dispatch), navegue o fluxo do cenário com Playwright e confira console e network. Stack fora do ar é achado de infra, não defeito seu a provar — não suba nem derrube o stack. Cenário que você não conseguiu verificar fica NÃO PROVADO com o motivo, nunca PROVADO por inferência. Siga a seção \"Verificador\" de `.morph/framework/evals/review-checklist.md` e grave `4-review/verifier.md` com `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, uma linha por cenário) — mais `## Checklist de tela`, `## Mídia gerada` e `## Mutações` quando o checklist pede. Cenário NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). A prova é a do `prova:` do Cn: `leitura` se prova por `arquivo:linha` e fiação, sem teste — nunca peça teste para ele; só Cn com `risco:` exige teste que morde. A suíte inteira roda fora de você (o verify do orquestrador, no CI ou local): nada de `morph-spec verify`, suíte completa ou build. Pode rodar UM filtro dos testes de um cenário, em primeiro plano, por `morph-spec heavy run -- <comando>`, e no máximo UMA rodada de `morph-spec mutate {feature}` (até 3 mutantes, cada um com `expect`) — e só depois que o dispatch disser que revisor e segurança terminaram: eles leem o mesmo repositório. Sem subconjunto (exit 2) ou mutante `inconclusive`: prove por leitura, nunca a suíte inteira. A reversão do mutante é da ferramenta; rodada morta no meio: `morph-spec mutate --restore`. Grave o relatório que o dispatch nomeia ANTES de encerrar — o arquivo é o retorno, não a mensagem final. Se ele já existir quando você começar, é resíduo de outra rodada: não o leia antes de escrever o seu (ancoragem no veredito alheio), e não leia os relatórios dos outros papéis. Primeira linha `**Verdict:** SHIP|FIX`, depois `**Reviewed-SHA:** {sha}`, tabela `## Bloqueadores` (`| id | tipo | onde | evidência |`; vazia = `Nenhum.`) e `## Notas`. Bloqueador só com evidência e tipo; o resto é nota, e nota nunca pede rodada nova. Numa confirmação (o orquestrador te retoma depois da correção, com o `sha` novo) acrescente ao fim uma seção nova com `**Verdict:**` e `**Reviewed-SHA:** {sha novo}`: se você abriu bloqueadores, julgue só eles; se não abriu, julgue só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. O relatório é append-only: vale o último veredito e, na `## Checks`, a última linha de cada Cn. Um comando por chamada de Bash, sem encadear (`;`, `&&`, pipe): o composto é negado e custa a rodada. Arquivo você lê com Read, nunca `sed`/`cat`/`head`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`). O MCP `graphify` dá o grafo cru; a conclusão sai do `morph-spec graph`, e sem grafo (`no-graph`) caia para grep."
371
+ "persona": "Você é o verificador independente do Gate 3. Não participou da implementação. Sua missão é PROVAR cada cenário `C{n}` do `2-plan/checks.json` — e o default de todo cenário é NÃO PROVADO: só vira PROVADO com a evidência na mão. Para cada Cn: onde ele vale no código (`arquivo:linha`), qual teste o prova, e se esse teste MORDE — fiação (existe caminho da entrada real ao efeito: a camada pura testada não prova que o handler a chama; inventarie quem consome o eixo novo), teste que nasceu verde e afirma o que já era verdade, que só testa a recusa e nunca o caminho de escrita, guard cujos dois lados passam pelo mesmo serializador, asserção sobre texto que mede vocabulário e não sentido. Verificações mecânicas (.NET minimal API / Next): todo serviço injetado numa lambda de endpoint tem registro em `Program.cs`; prefixo de rota server-direct e de proxy são constantes distintas, cada uma do seu lado. Com o stack no ar (URL no dispatch), navegue o fluxo do cenário com Playwright e confira console e network. Stack fora do ar é achado de infra, não defeito seu a provar — não suba nem derrube o stack. Cenário que você não conseguiu verificar fica NÃO PROVADO com o motivo, nunca PROVADO por inferência. Siga a seção \"Verificador\" de `.morph/framework/evals/review-checklist.md` e grave `4-review/verifier.md` com `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, uma linha por cenário) — mais `## Checklist de tela`, `## Mídia gerada` e `## Mutações` quando o checklist pede. Cenário NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). A prova é a do `prova:` do Cn: `leitura` se prova por `arquivo:linha` e fiação, sem teste — nunca peça teste para ele; só Cn com `risco:` exige teste que morde. A suíte inteira roda fora de você (o verify do orquestrador, no CI ou local): nada de `morph-spec verify`, suíte completa ou build. Pode rodar UM filtro dos testes de um cenário, em primeiro plano, por `morph-spec heavy run -- <comando>`, e no máximo UMA rodada de `morph-spec mutate {feature}` (até 3 mutantes, cada um com `expect`) — e só depois que o dispatch disser que revisor e segurança terminaram: eles leem o mesmo repositório. Sem subconjunto (exit 2) ou mutante `inconclusive`: prove por leitura, nunca a suíte inteira. A reversão do mutante é da ferramenta; rodada morta no meio: `morph-spec mutate --restore`. Arquivo você lê com Read, nunca `sed`/`cat`/`head`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`). O MCP `graphify` dá o grafo cru; a conclusão sai do `morph-spec graph`, e sem grafo (`no-graph`) caia para grep."
369
372
  },
370
373
  "reviewer": {
371
374
  "title": "Gate 3 Reviewer",
@@ -394,7 +397,7 @@
394
397
  ],
395
398
  "model": "sonnet",
396
399
  "maxTurns": 60,
397
- "persona": "Você é o revisor independente do Gate 3, read-only. Não participou da implementação e não prova cenários (é o verificador). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; `expectedCount` menor no `config.json` sem teste apagado no mesmo diff é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Grave o relatório que o dispatch nomeia ANTES de encerrar — o arquivo é o retorno, não a mensagem final. Se ele já existir quando você começar, é resíduo de outra rodada: não o leia antes de escrever o seu (ancoragem no veredito alheio), e não leia os relatórios dos outros papéis. Primeira linha `**Verdict:** SHIP|FIX`, depois `**Reviewed-SHA:** {sha}`, tabela `## Bloqueadores` (`| id | tipo | onde | evidência |`; vazia = `Nenhum.`) e `## Notas`. Bloqueador só com evidência e tipo; o resto é nota, e nota nunca pede rodada nova. Numa confirmação (o orquestrador te retoma depois da correção, com o `sha` novo) acrescente ao fim uma seção nova com `**Verdict:**` e `**Reviewed-SHA:** {sha novo}`: se você abriu bloqueadores, julgue só eles; se não abriu, julgue só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. O relatório é append-only: vale o último veredito e, na `## Checks`, a última linha de cada Cn. Um comando por chamada de Bash, sem encadear (`;`, `&&`, pipe): o composto é negado e custa a rodada. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
400
+ "persona": "Você é o revisor independente do Gate 3, read-only. Não participou da implementação e não prova cenários (é o verificador). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; `expectedCount` menor no `config.json` sem teste apagado no mesmo diff é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
398
401
  },
399
402
  "security": {
400
403
  "title": "Gate 3 Security Reviewer",
@@ -421,7 +424,7 @@
421
424
  "mcp": [],
422
425
  "model": "opus",
423
426
  "maxTurns": 60,
424
- "persona": "Você é a revisão de segurança do Gate 3, read-only, despachada só quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration. Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Procure, na seção \"Segurança\" de `.morph/framework/evals/review-checklist.md`: authz faltando ou checada do lado errado, segredo no código, no log ou no cliente, injeção (SQL, comando, prompt), dado pessoal vazando para log, resposta ou provedor, custo sem teto (chamada paga em laço, sem limite por usuário), mídia gerada publicada sem revisão humana, migration destrutiva (DROP/rename de coluna com dado, branch Neon de produção). Bloqueador é tipo `seguranca` e exige o CAMINHO DE EXPLORAÇÃO: quem, por qual entrada, chega a quê. Sem caminho, é nota. Grave o relatório que o dispatch nomeia ANTES de encerrar — o arquivo é o retorno, não a mensagem final. Se ele já existir quando você começar, é resíduo de outra rodada: não o leia antes de escrever o seu (ancoragem no veredito alheio), e não leia os relatórios dos outros papéis. Primeira linha `**Verdict:** SHIP|FIX`, depois `**Reviewed-SHA:** {sha}`, tabela `## Bloqueadores` (`| id | tipo | onde | evidência |`; vazia = `Nenhum.`) e `## Notas`. Bloqueador só com evidência e tipo; o resto é nota, e nota nunca pede rodada nova. Numa confirmação (o orquestrador te retoma depois da correção, com o `sha` novo) acrescente ao fim uma seção nova com `**Verdict:**` e `**Reviewed-SHA:** {sha novo}`: se você abriu bloqueadores, julgue só eles; se não abriu, julgue só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. O relatório é append-only: vale o último veredito e, na `## Checks`, a última linha de cada Cn. Um comando por chamada de Bash, sem encadear (`;`, `&&`, pipe): o composto é negado e custa a rodada. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
427
+ "persona": "Você é a revisão de segurança do Gate 3, read-only, despachada só quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration. Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Procure, na seção \"Segurança\" de `.morph/framework/evals/review-checklist.md`: authz faltando ou checada do lado errado, segredo no código, no log ou no cliente, injeção (SQL, comando, prompt), dado pessoal vazando para log, resposta ou provedor, custo sem teto (chamada paga em laço, sem limite por usuário), mídia gerada publicada sem revisão humana, migration destrutiva (DROP/rename de coluna com dado, branch Neon de produção). Bloqueador é tipo `seguranca` e exige o CAMINHO DE EXPLORAÇÃO: quem, por qual entrada, chega a quê. Sem caminho, é nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
425
428
  },
426
429
  "scout": {
427
430
  "title": "Codebase Scout",
@@ -29,12 +29,12 @@ If the user did not pass a feature name in `$ARGUMENTS`:
29
29
  ## What you display
30
30
 
31
31
  The CLI prints a banner, the phase chain (`plan → [uiux] → implement → review`),
32
- output completion (X/N outputs), slice progress (reports in `3-implement/reports/`), and next-step suggestions.
32
+ output completion (X/N outputs), slice progress (reports in `3-implement/reports/`), and the `próximo:` line (the same rule as `morph-spec next`).
33
33
 
34
34
  After running the CLI, summarize:
35
35
  - Current phase and which gate (if any) is open
36
36
  - Outstanding outputs for the current phase
37
- - Recommended next slash command (`/morph-plan`, `/morph-apply`, or a gate approval)
37
+ - The `próximo:` line as printed — never a next step of your own
38
38
 
39
39
  ---
40
40
 
@@ -262,6 +262,10 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
262
262
 
263
263
  **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
264
264
 
265
+ ### next
266
+
267
+ **`morph-spec next <feature> [--json]`** — o próximo passo do loop, decidido do disco na árvore dona (mesma resolução do `handoff`). Somente leitura. Ações: `plan` (sem `checks.json`), `gate` (plano extraído), `design` (uiux), `dispatch` (fatias prontas, com as `PARCIAL` nomeadas para retomar), `decide` (nenhuma pronta e alguma com relatório `BLOQUEADO`: decida pelo plano/ADR ou `AskUserQuestion` e retome o mesmo agente, nunca re-despache), `blocked` (pendentes sem dependência satisfeita), `verify` (todas relatadas e sem stamp, ou a árvore andou desde ele — `treeMovedSince`, a régua do `gate-check`), `fix`, `escalate`, `review`, `finish`. **Escalada:** `verify` persistido grava `verifications.feature.failStreak` (vermelhos seguidos; verde zera; rodada parcial ou `--no-persist` não conta); com o stamp vermelho e a árvore parada, `failStreak` múltiplo de 3 é `escalate` → `AskUserQuestion` (aceitar/continuar/parar); o `reason` diz o que cada resposta faz (aceitar → review, onde o verify vermelho pausa o gate e o humano assina). A resposta não é gravada: o `escalate` **persiste até a árvore andar** — releia a resposta da conversa em vez de perguntar de novo. "Continuar" segue de onde parou: a próxima escalada vem 3 vermelhos depois. **Por tipo:** `hotfix` troca toda skill pela `/morph-hotfix` (o pipeline inteiro mora lá); `chore`/`hotfix` nascem em implement sem `checks.json` → `plan` (plano curto e `plan-check`). A regra é UMA (`decideNext`, `framework/hooks/shared/next-step.js`) e tem três leitores: este comando, o `status` e o `handoff`, e o SessionStart, que injeta a linha `▶ próximo:` sem git (verde lido como fresco) — e por isso nunca injeta `escalate` (pede `morph-spec next`) nem o próximo passo do esqueleto da raiz de uma feature que vive num worktree (aponta o worktree). `--json`: `{ action, command, reason, slices?, phase, failStreak, feature, tree }`.
268
+
265
269
  ### fleet
266
270
 
267
271
  **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
@@ -472,7 +476,7 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
472
476
 
473
477
  **`morph-spec report-check <feature> [F1 F2 ...] [--before-eval] [--json]`** — audita o relatório de cada fatia (`3-implement/reports/F{n}.md`) contra o trabalho que realmente entrou.
474
478
 
475
- Vereditos: `ok`; `report-stale` → atualize a partir de `observed`, NÃO re-despache; `report-incomplete` com `partial: true` (relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns`) e ação `resume`: retome o mesmo agente, NÃO carimbe DEGRADADO; `report-incomplete` sem marcador → `complete-degraded`; `work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-missing-evidence-unknown` quando não há telemetria. O marcador é lido de propósito, nunca inferido da falta de um item.
479
+ Vereditos: `ok`; `report-stale` → atualize a partir de `observed`, NÃO re-despache; `report-incomplete` com `partial: true` (relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns`) e ação `resume`: retome o mesmo agente, NÃO carimbe DEGRADADO; `report-blocked` (relatório com `BLOQUEADO` na 1ª linha — decisão que não é do agente, com a pergunta e as opções) e ação `ask-human`: NÃO re-despache; decida pelo plano/ADR ou pergunte por `AskUserQuestion` e retome o mesmo agente com a resposta (a fatia fica fora das prontas e o `advance` do implement recusa); `report-incomplete` sem marcador → `complete-degraded`; `work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-missing-evidence-unknown` quando não há telemetria. O marcador é lido de propósito, nunca inferido da falta de um item.
476
480
 
477
481
  **Evidência por agente:** `observed` vem do `events.jsonl` de toda árvore — o dono da fatia é o `agentId` do último `Write` do relatório, e contam os writes desse agente; sem `agentId` (relatório inline do orquestrador) contam os writes aos `arquivos:` da fatia. Writes do orquestrador sob `.morph/` não contam. Sem fatia na linha de comando, audita toda fatia do `checks.json`.
478
482
 
@@ -496,7 +500,7 @@ Sem fatia na linha de comando, a saída traz `reviews: { verifier, reviewer, sec
496
500
 
497
501
  ### stack-scan
498
502
 
499
- **`morph-spec stack-scan [<feature>] [--json]`** — varredura determinística do código contra `config.project.stack`: só os stacks configurados são julgados (sem `project.stack`, nenhuma regra roda e o resultado é `não auditado: project.stack ausente em .morph/config/config.json`, nunca "Em compliance" — exit 0; stack sem regras de varredura sai `não auditado` pelo mesmo motivo; código de um stack não configurado é só anotado). `dotnet`: TargetFramework abaixo de net10 e ORM/banco diferente (Dapper puro, SQL Server, MySQL) são **Bloqueante**; `Guid.NewGuid()`, repository genérico (`IRepository<T>`/`IUnitOfWork` — código morto: apagar a pasta `Repository/` e as 2 linhas de DI), classe `*Service`/`*Manager` sobre `DbContext` e SQL cru (`FromSqlRaw`, `ExecuteSqlRaw`, `SqlQueryRaw`, `ExecuteQuery`) são **Alto**; classe não-sealed é **Médio**; pacote `Microsoft.EntityFrameworkCore|AspNetCore|Extensions` abaixo da major 10 é **Baixo**. `nextjs`: frontend sem Next.js é Bloqueante; Pages Router sem `app/` e biblioteca de UI diferente (MUI, antd, Chakra, Bootstrap, Mantine) são Médio. Código de teste não conta; `node_modules`, `bin`, `obj`, `.morph`, `.claude` e os contêineres de worktree ficam fora; arquivo ilegível é anotado, nunca derruba. Sem código fonte = GREENFIELD. As contagens são de texto e superestimam consumidores — o relatório aponta `graph affected <símbolo> --uses-only`. Desvio Bloqueante/Alto vira nó da árvore de decisões da `morph-plan`. Com `<feature>`, grava `.morph/features/<f>/2-plan/stack-scan.md` (exit 2 se a feature não existe, 1 fora da árvore dona); sem, imprime o markdown.
503
+ **`morph-spec stack-scan [<feature>] [--json]`** — varredura determinística do código contra `config.project.stack`: só os stacks configurados são julgados (sem `project.stack`, nenhuma regra roda e o resultado é `não auditado: project.stack ausente em .morph/config/config.json`, nunca "Em compliance" — exit 0; stack sem regras de varredura sai `não auditado` pelo mesmo motivo; código de um stack não configurado é só anotado). `dotnet`: TargetFramework abaixo de net10 e ORM/banco diferente (Dapper puro, SQL Server, MySQL) são **Bloqueante**; `Guid.NewGuid()`, repository genérico (`IRepository<T>`/`IUnitOfWork` — código morto: apagar a pasta `Repository/` e as 2 linhas de DI), classe `*Service`/`*Manager` sobre `DbContext` e SQL cru (`FromSqlRaw`, `ExecuteSqlRaw`, `SqlQueryRaw`, `ExecuteQuery`), Semantic Kernel (pacote `Microsoft.SemanticKernel*` ou `[KernelFunction]` no código) e serviço de agente hospedado (`Azure.AI.Projects`, `Azure.AI.Agents.Persistent`) são **Alto** — os dois últimos são linhas da tabela de anti-padrões do `ai-agents-setup` §7, para a qual a migração aponta; classe não-sealed e SDK de IA (`Microsoft.Agents.AI*`, `Microsoft.Extensions.AI*`, `OpenAI`, `Google.GenAI`, `ModelContextProtocol`) com versão flutuante (`*` ou faixa) são **Médio**; pacote `Microsoft.EntityFrameworkCore|AspNetCore|Extensions` abaixo da major 10 é **Baixo**. `nextjs`: frontend sem Next.js é Bloqueante; Pages Router sem `app/` e biblioteca de UI diferente (MUI, antd, Chakra, Bootstrap, Mantine) são Médio. Código de teste não conta; `node_modules`, `bin`, `obj`, `.morph`, `.claude` e os contêineres de worktree ficam fora; arquivo ilegível é anotado, nunca derruba. Sem código fonte = GREENFIELD. As contagens são de texto e superestimam consumidores — o relatório aponta `graph affected <símbolo> --uses-only`. Desvio Bloqueante/Alto vira nó da árvore de decisões da `morph-plan`. Com `<feature>`, grava `.morph/features/<f>/2-plan/stack-scan.md` (exit 2 se a feature não existe, 1 fora da árvore dona); sem, imprime o markdown.
500
504
 
501
505
  ### tests-audit
502
506
 
@@ -818,6 +822,7 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
818
822
  | suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
819
823
  | git | ocorrências de `git status` e `git diff` na janela |
820
824
  | reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
825
+ | gates | por gate, dos eventos `Gate`: `decisions`, `paused`, `autoPassed`, `unapproved`, e o destino de cada pausa — `approvedAfterPause` (o humano aprovou sem nova decisão: o risco foi aceito como estava) ou `reworkedAfterPause` (uma decisão seguinte já não pausou pelo mesmo sinal: o sinal levou a mudança). `bySignal` dá os três por sinal (`signal` de `shouldPauseGate`, ou `manual`). Rodar o `gate-check` de novo sobre a mesma pausa não conta como retrabalho. Sinal com muito `approvedAfterPause` e pouco `reworkedAfterPause` é candidato a afrouxar — a decisão é humana, o número só a informa |
821
826
 
822
827
  Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit code ou duração, eles caem em "sem agente"/`unknown-*`/"sem duração", e o cabeçalho diz que fração da janela está no esquema v2.
823
828
 
@@ -830,6 +835,7 @@ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit cod
830
835
  | `PostToolUse` / `PostToolUseFailure` | `tool`, `toolUseId`, `durationMs`; Bash/PowerShell: `command` (redigido, ≤2000 chars) + `commandChars`, `exitCode`, `outcome` (`ok`/`error`/`timeout`/`interrupted`/`background`), `exitMasked` quando o exit gravado não é o do trabalho (pipe sem `pipefail`, `|| …`, `&` em background, ou `;` seguido de `echo`/`true`/`:` no fim); Read: `target`, `kind`, `bytes` (tamanho do arquivo), `offset`, `limit`, `bytesRead` — um Read que falhou não grava bytes, porque não trouxe nada; Agent: `persona`, `desc`, `prompt` + `promptChars`; SendMessage: `target`, `messageChars` |
831
836
  | `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
832
837
  | `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
838
+ | `Gate` | `gate`, `step` (`decision`/`approve`/`unapprove`); na `decision`: `pause`, `signal` (a regra que pausou, `null` sem pausa, `manual` pelo `gate-decision`) e `source`; nas outras: `approvedBy`. Gravado por `persistGateDecision` e pelos dois escritores de aprovação (CLI e hook `state-sync`) |
833
839
  | `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
834
840
 
835
841
  Nada da saída de um comando é gravado — nem o corpo de erro do `PostToolUseFailure`, onde um valor de `.env` pode aparecer sem nome ao lado. `command`, `prompt`, `desc` e `target` passam por redação de segredos óbvios antes de gravar: valor ao lado de um nome de segredo (`API_KEY=…`, `*_KEY`/`*_PASS=…`, `--password …`, `user-secrets set <k> <v>`, `AccountKey=…`), `Authorization: …`, `user:pass@` (senha com `/` inclusive) e `?sig=`/`?access_token=` em URL, e formatos de token (GitHub, `sk-`, `sk_live_`, `npm_`, Slack, AWS, Google, JWT). É por padrão, não por garantia: um valor sem nome ao lado passa.
@@ -129,6 +129,9 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
129
129
  5. **Performance só com evidência**: lista sem paginação no servidor sobre volume real, N+1 visível.
130
130
  6. **Chamada a LLM**: dá para reconstruir o que foi ao provedor e o que ele respondeu neste turno?
131
131
  Não → nota.
132
+ 7. **Tela no diff**: cor ou espaçamento literal fora do CSS de tokens, mais de uma ação primária,
133
+ erro sem próxima ação, conteúdo gerado por IA sem marca, foco invisível → nota. Bloqueia só pelo
134
+ contrato de tela do verificador.
132
135
 
133
136
  ## Segurança (`4-review/security.md`)
134
137