@polymorphism-tech/morph-spec 10.7.1 → 10.8.1

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 (57) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +15 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/MORPH.md +4 -2
  7. package/framework/agents.json +1 -1
  8. package/framework/docs/CLI.md +32 -17
  9. package/framework/hooks/README.md +2 -2
  10. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +12 -4
  11. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +43 -4
  12. package/framework/hooks/claude-code/core/conventional-commit.js +1 -1
  13. package/framework/hooks/claude-code/core/protect-spec-files.js +8 -1
  14. package/framework/hooks/claude-code/core/worktree-context.js +5 -1
  15. package/framework/hooks/shared/command-tokens.js +1 -1
  16. package/framework/hooks/shared/e2e-check.js +16 -0
  17. package/framework/hooks/shared/heavy-command.js +3 -2
  18. package/framework/hooks/shared/state-reader.js +3 -0
  19. package/framework/rules/implement-report.md +1 -1
  20. package/framework/rules/plan-grammar.md +3 -2
  21. package/framework/schemas/config.schema.json +6 -1
  22. package/framework/schemas/feature.schema.json +4 -2
  23. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  24. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  25. package/framework/skills/core/morph-apply/SKILL.md +8 -7
  26. package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
  27. package/framework/skills/core/morph-apply/references/review.md +9 -3
  28. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  29. package/framework/skills/core/morph-plan/references/critique.md +26 -18
  30. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  31. package/framework/templates/context/ARCHITECTURE.md +0 -3
  32. package/package.json +1 -1
  33. package/src/commands/graph/diff.js +9 -2
  34. package/src/commands/project/worktree.js +86 -7
  35. package/src/commands/state/approve.js +4 -7
  36. package/src/commands/state/create.js +3 -3
  37. package/src/commands/state/finish.js +75 -4
  38. package/src/commands/state/gate-check.js +7 -2
  39. package/src/commands/state/report-check.js +33 -6
  40. package/src/commands/validation/plan-check.js +11 -2
  41. package/src/commands/validation/prove-red.js +95 -0
  42. package/src/commands/validation/verify.js +56 -40
  43. package/src/lib/ci/ci-verify.js +1 -106
  44. package/src/lib/ci/github-ci.js +5 -117
  45. package/src/lib/generators/architecture-scanner.js +1 -14
  46. package/src/lib/git/default-branch.js +28 -1
  47. package/src/lib/plan/artifact-vocabulary.js +7 -3
  48. package/src/lib/plan/critique.js +17 -0
  49. package/src/lib/review-confirmation.js +10 -11
  50. package/src/lib/review-reports.js +12 -3
  51. package/src/lib/risk/diff-risk.js +137 -0
  52. package/src/lib/risk-detector.js +1 -1
  53. package/src/lib/validators/code-budget/code-budget-validator.js +16 -4
  54. package/src/lib/validators/verify-runner.js +24 -2
  55. package/src/lib/verify/affected.js +3 -1
  56. package/src/utils/refusal-line.js +37 -0
  57. package/src/lib/ci/workflow-scaffold.js +0 -126
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:** 10.7.1
6
+ **Version:** 10.8.1
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
376
376
 
377
377
  ---
378
378
 
379
- *morph-spec v10.7.1 by [Polymorphism Tech](https://polymorphism.tech)*
379
+ *morph-spec v10.8.1 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -56,6 +56,7 @@ const graphExplainCommand = lazy('../src/commands/graph/explain.js', 'graphExpla
56
56
  const graphDiffCommand = lazy('../src/commands/graph/diff.js', 'graphDiffCommand');
57
57
 
58
58
  const verifyCommand = lazy('../src/commands/validation/verify.js', 'verifyCommand');
59
+ const proveRedCommand = lazy('../src/commands/validation/prove-red.js', 'proveRedCommand');
59
60
  const e2eUpCommand = lazy('../src/commands/e2e/e2e.js', 'e2eUpCommand');
60
61
  const e2eStatusCommand = lazy('../src/commands/e2e/e2e.js', 'e2eStatusCommand');
61
62
  const e2eDownCommand = lazy('../src/commands/e2e/e2e.js', 'e2eDownCommand');
@@ -331,7 +332,7 @@ program
331
332
  .option('--fresh-e2e', 'Recreate the e2e stack from scratch: down -v (named volumes included), drop the freshness stamp, rebuild, re-seed auth')
332
333
  .option('--fresh', 'Run every node again, ignoring the green nodes of an earlier run on this same tree. Use it when something outside the tree changed (.env, database, tool version)')
333
334
  .option('--no-persist', 'Do not persist the result to feature.json')
334
- .option('--ci', 'Push morph/<feature> and take the proof from the project CI (creates .github/workflows/morph-verify.yml when no workflow runs verify). Same stamp, source "ci". No GitHub/gh/workflow = local verify with the reason; uncommitted code = exit 2; CI still running after 50 min = exit 3, no stamp')
335
+ .option('--ci', 'Push morph/<feature> and take the proof from the project CI; never creates a workflow or a commit. Same stamp, source "ci". No GitHub/gh/workflow = local verify with the reason; uncommitted code = exit 2; CI still running after 50 min = exit 3, no stamp')
335
336
  .option('--affected', 'Run only the tests the diff reaches, with build and validators and without e2e. Keeps its own record: the review gate still asks for one complete run. Migration, contract, project-wide files or code shared by too many tests run complete by themselves')
336
337
  .option('--detach', 'Run the verify in its own process, out of the calling session: a killed session or background shell does not take the run with it. This call waits and prints the same result; the same command reattaches')
337
338
  .action((feature, options) =>
@@ -529,6 +530,14 @@ program
529
530
  .option('--json', 'Output as JSON')
530
531
  .action((options) => standardsCommand(options));
531
532
 
533
+ program
534
+ .command('prove-red <feature>')
535
+ .description('Mutation proof of a scenario: run its test with the change, put the feature production files back to the merge-base, run it again, restore. Exit 0 = red without the change; 1 = still green (proves nothing); 2 = already red or nothing to revert')
536
+ .requiredOption('--test <command>', 'The scenario test, filtered to it (the command the report.md cites)')
537
+ .option('--files <paths>', 'Comma-separated production files of this scenario (default: every production file the feature changed)')
538
+ .option('--json', 'Output as JSON')
539
+ .action((feature, options) => proveRedCommand(feature, options));
540
+
532
541
  program
533
542
  .command('report-check <feature>')
534
543
  .description('State of the Gate 3 reports (4-review/), scenario progress of 3-implement/report.md and the tree they were written against')
@@ -583,6 +592,11 @@ program
583
592
  // The page follows every command that moves the feature; 'exit' also covers their process.exit().
584
593
  const VIEW_COMMANDS = new Set(['plan-check', 'gate-check', 'gate-decision', 'approve', 'unapprove', 'advance', 'verify', 'report-check']);
585
594
  program.hook('preAction', async (_, action) => {
595
+ // Detached verify stages report through the supervisor, which prints the line once.
596
+ if (!process.env.MORPH_VERIFY_STAGE) {
597
+ const { installRefusalLine } = await import('../src/utils/refusal-line.js');
598
+ installRefusalLine(action.parent?.name() && action.parent !== program ? `${action.parent.name()} ${action.name()}` : action.name());
599
+ }
586
600
  const [feature] = action.args;
587
601
  if (!VIEW_COMMANDS.has(action.name()) || !/^[a-z0-9][\w-]*$/i.test(feature ?? '')) return;
588
602
  const { refreshesFeatureView } = await import('../src/lib/verify/detached.js');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "10.7.1",
3
+ "version": "10.8.1",
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",
@@ -174,4 +174,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
174
174
 
175
175
  ---
176
176
 
177
- *morph-spec v10.7.1 by Polymorphism Tech*
177
+ *morph-spec v10.8.1 by Polymorphism Tech*
@@ -89,4 +89,4 @@ morph-spec doctor
89
89
 
90
90
  ---
91
91
 
92
- *morph-spec v10.7.1 by Polymorphism Tech*
92
+ *morph-spec v10.8.1 by Polymorphism Tech*
@@ -46,14 +46,16 @@ Na abertura do plano vale o contrário: confronte o pedido com o código e pergu
46
46
  | --- | --- | --- |
47
47
  | `feature` | plan → [uiux] → implement → review | plano, [uiux], Gate 3 |
48
48
  | `bug` | plano enxuto (repro + causa) → implement → review | plano, Gate 3 |
49
- | `chore` | plano curto carimbado → implement → só revisor | Gate 3 |
49
+ | `chore` | plano curto (congela no 1º `plan-check` verde) → implement | Gate 3 |
50
50
  | `hotfix` | plano curto carimbado → fix → `verifier.md` seu → humano | Gate 3, sempre humano |
51
51
 
52
+ **A cerimônia segue o risco do diff, não o tipo** (`report-check --before-eval` → `reviewModels.risk`). Baixo (doc, `.morph/`, config, remoção, import; prompt de agente nunca é doc): sem worktree, sem sub-agent (você escreve o `reviewer.md` pelo checklist), `verify --affected` é a prova. Lógica em código ou teste: verificador ∥ revisor. PII, permissão, envio a terceiro, pagamento ou contrato: + segurança.
53
+
52
54
  Toda feature nasce em `morph/{f}` e fecha com `morph-spec finish <f> --pr|--merge`.
53
55
 
54
56
  ### 2.2 Desenvolvimento paralelo por worktree (frota)
55
57
 
56
- Features independentes rodam em paralelo, **uma feature = um worktree = uma sessão**: `create <f> --worktree`. Dentro de uma feature, nunca paralelo.
58
+ Features independentes rodam em paralelo, **uma feature = um worktree = uma sessão**: `create <f> --worktree` (nasce com links e `dotnet restore`). Dentro de uma feature, nunca paralelo.
57
59
 
58
60
  - **Só o morph cria worktree:** `worktree setup <f>` (feature) ou `worktree provision <n>` (árvore avulsa); `EnterWorktree`/`claude -w` passam pelo hook `WorktreeCreate`. Já existe: abra o `claude` dentro dela.
59
61
  - **Só o morph remove worktree.** `finish` ou `worktree remove`; qualquer outra remoção atravessa a junction e apaga a raiz.
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "model": "sonnet",
33
33
  "maxTurns": 90,
34
- "persona": "Você é o verificador independente do Gate 3. Não participou da implementação e não recebe o plano, o diff nem o relato de quem implementou — de propósito: sua missão é FAZER cada cenário `C{n}` do `checks.json` acontecer e dizer se o resultado é o que ele promete. O default de todo cenário é NÃO PROVADO; o que você não executou (ou, em `prova: leitura`, não leu no código) nunca vira PROVADO por inferência. Pela `prova:` do Cn: `e2e` → navegue o fluxo com o Playwright MCP na URL do dispatch como um usuário, conferindo tela, console e network, e rode `morph-spec e2e reset {feature}` ANTES de cada fluxo; `teste` → ache o teste do Cn e rode só ele pelo filtro do runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`; `verify --filter` escolhe um nó inteiro e só serve quando o nó é esse teste), depois leia se ele MORDE (nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, assert que passa pelo motivo errado); `eval` → rode o nó de evals filtrado e compare a saída com o cenário; `leitura` → `arquivo:linha` e a fiação da entrada real ao efeito, sem pedir teste; `manual` → reproduza o passo de `3-implement/evidencias/{Cn}.md`. Verificações mecânicas (.NET minimal API / Next): serviço injetado numa lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como constantes distintas. A suíte completa roda depois de você: nunca `verify` sem filtro nem a suíte completa (o hook `test-budget` nega). Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — não suba nem derrube o stack. 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; a evidência é o que você executou e viu) — mais `## Checklist de tela` e `## Mídia gerada` quando o checklist pede. NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). 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`). Sem grafo (`no-graph`), caia para grep."
34
+ "persona": "Você é o verificador independente do Gate 3. Não participou da implementação e não recebe o plano, o diff nem o relato de quem implementou — de propósito: sua missão é FAZER cada cenário `C{n}` do `checks.json` acontecer e dizer se o resultado é o que ele promete. O default de todo cenário é NÃO PROVADO; o que você não executou (ou, em `prova: leitura`, não leu no código) nunca vira PROVADO por inferência. Pela `prova:` do Cn: `e2e` → navegue o fluxo com o Playwright MCP na URL do dispatch como um usuário, conferindo tela, console e network, e rode `morph-spec e2e reset {feature}` ANTES de cada fluxo; `teste` → ache o teste do Cn e rode só ele pelo filtro do runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`, ou o script de bancada do plano com o filtro dele, `scripts/test-pg.ps1 -Filter <nome>`; `verify --filter` escolhe um nó inteiro e só serve quando o nó é esse teste), depois leia se ele MORDE (nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, assert que passa pelo motivo errado); `eval` → rode o nó de evals filtrado e compare a saída com o cenário; `leitura` → `arquivo:linha` e a fiação da entrada real ao efeito, sem pedir teste; `manual` → reproduza o passo de `3-implement/evidencias/{Cn}.md`. Verificações mecânicas (.NET minimal API / Next): serviço injetado numa lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como constantes distintas. A suíte completa roda depois de você: nunca `verify` sem filtro nem a suíte completa (o hook `test-budget` nega). Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — não suba nem derrube o stack. 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; a evidência é o que você executou e viu) — mais `## Checklist de tela` e `## Mídia gerada` quando o checklist pede. NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). 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`). Sem grafo (`no-graph`), caia para grep."
35
35
  },
36
36
  "reviewer": {
37
37
  "title": "Gate 3 Reviewer",
@@ -36,6 +36,8 @@
36
36
 
37
37
  No **review**, o `gate-check` lê os relatórios de `4-review/` (`src/lib/review-reports.js` — leitor único) e grava `signals.reviewReports = {required, reports: {verifier, reviewer, security}, checks}`: exigidos são `verifier` e `reviewer`, mais `security` quando o diff toca caminho sensível; no `hotfix`, só `verifier`. Pausa o Gate 3: relatório exigido ausente, PARCIAL ou sem `**Verdict:**`; `FIX`; bloqueador aberto; bloqueador sem tipo (`check`/`teste`/`regressao`/`seguranca`/`escopo`/`arquitetura`/`performance`/`irreversivel`; um `irreversivel` aberto vai direto ao humano); e a cobertura — todo cenário do `checks.json` precisa de uma linha `PROVADO` na `## Checks` do `verifier.md` — somadas todas as seções, vale a última linha de cada Cn (`checks.missing` = sem linha, `checks.notProven` = `NÃO PROVADO`/`FALHA`), e `checks.json` ilegível também pausa. Cada relatório é append-only e vale o ÚLTIMO `**Verdict:**`. `reviewReports` não é sobrescrevível por `--signals`. Fora do review a regra não vale.
38
38
 
39
+ **Papéis do Gate 3 pelo risco do diff** (`src/lib/risk/diff-risk.js`, do merge-base com `origin/<default>`): `low` (só doc, `.morph/`, config, remoção ou import; prompt de agente — `prompts/`, `agents/`, `skills/`, `playbooks/`, `.claude/`, `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, `*prompt*` e os globs de `project.evals.watch` — é código, e qualquer linha dele é lógica) exige só o `reviewer.md`, escrito pela sessão com o checklist; `normal` (lógica em código ou teste, migration, arquivo que alcança o projeto inteiro) exige verificador e revisor, inclusive num chore; PII, permissão, envio a terceiro, pagamento ou contrato, pelo caminho (camelCase também: `PiiPatterns.cs`) ou por linha adicionada, acrescentam a segurança. Hotfix segue humano. O `--json` traz `risk`. A revisão velha tem um oráculo só (`reviewConfirmation`), lido pelo `approve` e pelo `report-check`: delta trivial (`.morph/`, doc, config, import, comentário) não invalida relatório; um bloqueador do relatório relistado como **Fechado** na confirmação não bloqueia.
40
+
39
41
  Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
40
42
 
41
43
  **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `not-applicable` ("not applicable: …", a feature sem cenário `prova: e2e` e sem arquivo de UI, spec e2e ou container no diff) são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. `e2eStatus`/`e2eReason`/`e2eSkipClass` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`.
@@ -127,6 +129,10 @@ Feature sem `workType` (pré-v8) é tratada como `feature`.
127
129
 
128
130
  **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum; a sessão escolhe por `finish.mode` do `config.json`, padrão `pr`): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
129
131
 
132
+ **`--pr` junta a default antes do push:** `fetch` + `merge origin/<default>` na branch da feature, antes do archive. Conflito só em arquivo gerado (`.morph/context/README.md`, que não carrega data) fica com a cópia da default; qualquer outro conflito desfaz o merge e recusa (`error: "default-conflict"`, `conflicts: [...]`, exit 1) sem arquivar nem enviar nada. O corpo do PR é a `description`; um link de issue nela vira `Closes owner/repo#N`, a menos que a própria description já traga a palavra (`Refs #N` mantém a issue aberta).
133
+
134
+ **Antes de qualquer mutação**, o `finish` recusa com `worktree-in-use` quando outro processo tem a worktree como pasta de trabalho (no Windows, um rename de prova falha): um terminal, o editor ou um servidor de dev deixariam a pasta órfã depois do archive. A âncora da própria sessão não recusa, adia o desmonte. O `--pr` junta `origin/<default>` antes do push (conflito só em arquivo gerado, como `.morph/context/README.md`, resolve com a versão do default; outro recusa com `default-conflict` e a lista), e o corpo do PR leva `Closes owner/repo#N` quando a `description` tem o link da issue.
135
+
130
136
  **Stamp com `pendingPr`** (`verify --ci` sob `project.prChecks`): o `--pr` espera os checks declarados no commit do archive, antes do desmonte, porque é esse o run que o merge reaproveita; provar antes do archive pagaria o CI duas vezes. Rode em `run_in_background`. `pass` → `checks: { status: "pass" }`. Check vermelho → `error: "pr-checks-fail"`: desfaz o archive com `git revert`, reabre o Gate 3, carimba `status: "fail"`, `source: "pr"`, devolve a PR a rascunho (o revert fica local, e rascunho não mergeia) e sai 1. Check que morreu em menos de 30 s (cota do Actions), cancelado, ou nenhum check no commit em 90 s → `error: "pr-checks-no-verdict"`: desfaz igual, apaga o stamp, e a saída é a completa local, `verify {f}` sem `--ci`. O próximo `finish` reaproveita a PR aberta. Sem resposta em 50 min: `finished: true`, `checks: { status: "pending" }`, sem desfazer nada. Processo morto na espera: o mesmo `finish --pr` retoma. `--merge` recusa esse stamp (`pr-proof-pending`).
131
137
 
132
138
  **Índice de ADRs:** no mesmo commit do archive, o `finish` lê o `2-plan/plan.md` da feature: os ADRs de `## Decisões` com `**Alcance:** projeto` entram em `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo), e a tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` retira (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os ADRs revogados — forma em `morph-spec plan-check --help`; o `plan-check` avisa, enquanto o plano ainda muda, a linha que o `finish` não aplicaria. O payload traz `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`.
@@ -171,6 +177,8 @@ Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed:
171
177
 
172
178
  O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a cada load (self-heal) e nunca é sincronizado à mão.
173
179
 
180
+ **Pronta para build:** a branch nova sai de `origin/<default>` depois de um `fetch` (nunca de uma default local que não foi puxada), e o `setup` roda `dotnet restore` na solução da raiz (`restore` na saída; `MORPH_WORKTREE_RESTORE=0` desliga), porque a árvore nova não tem `obj/` e o primeiro build cairia em NETSDK1004. O `node_modules` vem por junction da raiz; o SessionStart só cobra o da raiz quando a raiz primária tem um.
181
+
174
182
  **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
175
183
 
176
184
  **Base da árvore de task.** O hook cria a árvore de task sempre a partir da branch default. `provision --base morph/{f}` (à mão) grava `MORPH_FEATURE={f}` no `worktree.env` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
@@ -187,6 +195,8 @@ O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a
187
195
 
188
196
  Derruba o stack compose da árvore e os `e2e.hostServices`: de branch `morph/{f}` **sem** apagar volumes (a feature segue aberta, `down --remove-orphans`); de branch `morph-task/*` **com** volumes (`down --remove-orphans -v`: a task é descartável). Libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria).
189
197
 
198
+ **Pasta órfã** (`--target` numa pasta dentro de `worktrees/` que o git não conhece mais, deixada por um desmonte que bateu num cwd travado): cada link sai como link (`unlink`/`rmdir`, nunca seguindo a junction) e cada pasta vazia por `rmdir`, sem recursão; arquivo de verdade lá dentro recusa (`orphan-not-empty`, com a lista) antes de tocar em qualquer coisa; pasta em uso sai `orphan-in-use`. Git rodando numa pasta dessas cai na raiz: o hook `block-raw-worktree-add` o bloqueia e nomeia este comando.
199
+
190
200
  **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
191
201
 
192
202
  **Nada é commitado na branch principal por causa do esqueleto da feature na raiz:** o registro `worktree` é apagado do `feature.json`, e só um registro que já estava **rastreado** vira commit. O esqueleto não rastreado de uma feature já arquivada na raiz é apagado com backup (`shedBackup`); o de uma feature sem archive fica como está.
@@ -327,6 +337,8 @@ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de con
327
337
 
328
338
  **Sobe sozinha para a completa**, com a razão no `feedback`, e aí carimba `verification` normalmente: migration, contrato (`openapi`, `swagger`, `.proto`), arquivo que alcança o projeto inteiro (`package.json`, lockfile, `.csproj`, `Directory.*.props`, `global.json`, `tsconfig`, `vitest.config`, fora dos projetos de teste), código que alcança mais de 40 classes de teste, ou sem base de comparação.
329
339
 
340
+ **Quando a afetada é a prova:** diff de baixo risco → a rodada verde grava o `verification` (`scope: "affected"`, e2e dispensado como `not applicable`). Com um carimbo verde da completa, a afetada verde move o carimbo para o HEAD (`renewedBy: "affected"`) quando o delta desde ele não toca spec e2e: um rename de fixture não paga outra completa. Só árvore commitada. Mesmo tree já verde (completa ou afetada) → `stamp-reused`, sem rodar (`--fresh` força). Seleção que não pulou nem estreitou suíte nenhuma É a completa: roda e carimba como ela, e o `--ci` depois dos relatórios a reaproveita. A completa, por sua vez, pula a suíte com `cwd` próprio que o diff da feature não alcança (`suítes fora do diff da feature`), pela mesma seleção; suíte com `cwd` na raiz roda sempre.
341
+
330
342
  **Seleção por nó:** `vitest` ganha `--changed <base> --passWithNoTests`; `dotnet` filtra pelas classes de teste que citam os tipos mudados (`git grep -lw`, um salto só) mais os testes mudados; nó `e2e` e `evals` (também o `dotnet` chamado `evals`, porque custa dinheiro) ficam para a completa; os outros rodam inteiros se algo mudou no `cwd` deles. Nó sem nada a rodar sai pulado. **Nada a configurar:** filtro já no comando é combinado com a seleção; `.ps1` com `[string] $Filter` que chega ao `dotnet test` usa `-Filter` e guarda o padrão do parâmetro; script que repassa os argumentos (`"$@"`, `$args`) recebe `--filter`. Só um comando que nada disso alcança roda inteiro, ou declara `affected: { flag, baseFilter }` em `project.tests[]`, que vence a detecção. Só doc ou imagem no diff: pula build e testes. Não combina com `--ci`, `--filter`, `--record-baseline`, `--refresh-cache` nem `--linux`.
331
343
 
332
344
  ### verify --detach
@@ -335,27 +347,19 @@ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de con
335
347
 
336
348
  ### verify --ci
337
349
 
338
- **`morph-spec verify <feature> --ci [--json]`** — a prova de escopo de feature tirada do **CI do projeto** (GitHub Actions), não da máquina: faz `git push -u origin morph/<feature>`, espera o run do commit e transforma o `verify --json` que o run publicou (artefato `morph-verify`) no **mesmo stamp** de uma rodada local, com `source: "ci"` e `ciRun` (URL do run); `sha` é o `headSha` do run, então o `verificationStale` compara o código que o CI viu com o HEAD.
339
-
340
- Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
341
-
342
- **Sem workflow** que rode `morph-spec verify` publicando `morph-verify.json`, cria e commita `.github/workflows/morph-verify.yml` na branch da feature (gatilho `push` em `morph/**`, `.morph/` fora do gatilho exceto o `config.json`, `cancel-in-progress`, setup de .NET por `global.json`/stack, install por lockfile, browsers do Playwright onde há config, e o `morph-spec` na versão de `.morph/.morphversion`). Um run sobre um commit anterior vale quando só `.morph/` andou desde ele; run `cancelled`/`skipped`/`startup_failure` não é prova.
350
+ **`morph-spec verify <feature> --ci [--json]`** — a prova completa da feature sem pagar duas vezes o que o CI da PR já roda. **Nunca empurra, commita nem cria workflow:** o único run de CI por SHA é o da PR que o `finish --pr` abre.
343
351
 
344
- **Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de tudo — com ou sem CI — reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não empurra, não roda nem regrava, imprime "stamp verde e fresco — o CI não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
352
+ **Antes de tudo reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não roda nem regrava, imprime "stamp verde e fresco — não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
345
353
 
346
- Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
354
+ Sem `project.prChecks` (ou com `project.ci: false`, `finish.mode: "merge"`) é o verify local completo, com a razão. Com `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso.
347
355
 
348
- **O runner só tem o que está commitado:** o que testes e e2e leem de arquivo gitignored (`.env`, `appsettings.*.json` local, connection string do Neon, `storageState`) não existe lá, e o run sai vermelho sem defeito de código. Declare o valor como secret do repositório e exponha-o no `env:` do job em `morph-verify.yml` (o arquivo é do projeto: editar à mão é o esperado, e o `findCiWorkflow` continua aceitando enquanto ele chamar `morph-spec verify` publicando `morph-verify.json`), ou desligue com `project.ci: false`.
349
-
350
- **Por quê.** Só o **artefato** vira prova, nunca o "verde" do run: workflow de lint, deploy ou de terceiro não compra o Gate 3.
351
-
352
- **Prova na PR (`project.prChecks`):** projeto cujo CI de PR já roda a suíte declara `"prChecks": { "workflows": ["Backend", "Frontend"], "covers": ["dotnet", "vitest"] }`, e o `--ci` não cria `morph-verify.yml` nem paga a suíte duas vezes: roda local só o que a PR não prova (build, validadores, e2e aplicável, nós fora de `covers`) e carimba com `pendingPr`. Quem espera é o `finish --pr` (`### finish`). `workflows` é o `name:` do workflow; workflow fora da lista não prova nada. `covers` vale pelo que o workflow roda de fato: Vitest só com `--changed` na PR é cobertura menor aceita pelo projeto. Roda a completa local, com a razão, o que a rodada afetada subiria (migration, contrato, arquivo que alcança o projeto inteiro, código compartilhado) e o que não alcança o CI (`ciAvailability`). Ignorado com `project.ci: false` ou `finish.mode: "merge"`.
356
+ **Prova na PR (`project.prChecks`):** projeto cujo CI de PR já roda a suíte declara `"prChecks": { "workflows": ["Backend", "Frontend"], "covers": ["dotnet", "vitest"] }`, e o `--ci` não paga a suíte duas vezes: roda local só o que a PR não prova (build, validadores, e2e aplicável, nós fora de `covers`) e carimba com `pendingPr`. Quem espera é o `finish --pr` (`### finish`). `workflows` é o `name:` do workflow; workflow fora da lista não prova nada. `covers` vale pelo que o workflow roda de fato: Vitest só com `--changed` na PR é cobertura menor aceita pelo projeto. Roda a completa local, com a razão, o que a rodada afetada subiria (migration, contrato, arquivo que alcança o projeto inteiro, código compartilhado) e o que não alcança o CI (`ciAvailability`). Ignorado com `project.ci: false` ou `finish.mode: "merge"`.
353
357
 
354
358
  ### Validador code-budget
355
359
 
356
360
  **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`).
357
361
 
358
- Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário).
362
+ Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); cada `Cn` com `prova: teste` (que sempre leva `risco:`) desconta **40 linhas**, porque o teste que a régua exige não é excesso; sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário).
359
363
 
360
364
  Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas.
361
365
 
@@ -369,6 +373,8 @@ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falh
369
373
 
370
374
  `--fresh` derruba com volumes (`down -v`), apaga o carimbo, reconstrói e reinicia os `e2e.hostServices` e re-roda o seed.
371
375
 
376
+ **Quarentena (`e2e.quarantine`):** spec fora do escopo da feature e sabidamente vermelho (corrida com o spec de outra feature) entra como trecho da linha de falha do Playwright. Vermelho depois do retry único, ele não reprova o nó e fica nomeado no feedback e no `flaky` do carimbo; spec vermelho que a lista não nomeia continua reprovando.
377
+
372
378
  `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do verificador, em vez de deixá-lo adivinhar `localhost:3000`.
373
379
 
374
380
  `down [--volumes]` é best-effort e nunca bloqueia; derruba também os `e2e.hostServices`.
@@ -516,12 +522,18 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
516
522
 
517
523
  **Por quê.** Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas.
518
524
 
525
+ ### prove-red
526
+
527
+ **`morph-spec prove-red <feature> --test "<comando>" [--files a,b] [--json]`** — a prova "veja falhar sem a mudança" de um `Cn` com `prova: teste`, feita pela CLI: roda o teste do cenário com a mudança (tem de passar), devolve os arquivos de **produção** do diff da feature (`--files` restringe aos do cenário) ao conteúdo da merge-base com a default, roda de novo e restaura sempre (`finally`, da cópia em memória; arquivo que a feature criou some e volta). Teste nunca é revertido. Exit 0 = vermelho sem a mudança (o teste morde); 1 = continua verde (filtro errado, ou asserção que já era verdade: não prova o cenário); 2 = já vermelho com a mudança, ou nada a reverter. O `--json` traz `withChange`, `withoutChange` e `reverted`, que vão como evidência na linha do `Cn` no `report.md`.
528
+
519
529
  ### report-check
520
530
 
521
531
  **`morph-spec report-check <feature> [--before-eval] [--json]`** — o estado do implement e dos relatórios do Gate 3. Sempre exit 0; sem `checks.json` recusa (exit 1) nomeando o `plan-check`.
522
532
 
523
533
  `implement: { total, done, pending, blocked, legacy, warnings }` sai de `3-implement/report.md` — a tabela `| Cn | status | evidência |` (`pendente`, `feito`, `bloqueado`), régua única em `hooks/shared/report-progress.js`; status fora do vocabulário conta como pendente. Feature anterior à 10.0, sem `report.md`, conta pelos relatórios de fatia (`legacy: true`). `reviews: { verifier, reviewer, security }`, cada um `{ exists, complete, partial, verdict }`: `complete` = veredito legível e sem `PARCIAL` na 1ª linha; `partial` = o agente bateu no teto de turnos — despache o mesmo papel de novo dizendo o que já está no relatório.
524
534
 
535
+ `--before-eval` também traz `reviewModels.risk` e `reviewModels.selfReview` (baixo risco: a sessão escreve o `reviewer.md`, sem sub-agent); com relatório já no disco o dispatch é confirmação e o verificador vai sempre em `sonnet`. Relatório de Gate 3 que um agente gravou e não commitou vira um commit só dele (`committedReports`), antes de qualquer outro commit o carregar.
536
+
525
537
  **Bloco `tree`**, em toda saída e sem mudar o exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde — desconhecido, nunca árvore limpa.
526
538
 
527
539
  **`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`. Traz ainda `gate3: { base, sha, flaky, e2eChecks, redispatch }`: `base` é o merge-base com a default, `flaky` vem do stamp do verify, `e2eChecks` lista os `Cn` de `prova: e2e`, e `redispatch` diz quem volta depois de uma correção (`null` sem relatório: todos os exigidos vão).
@@ -534,6 +546,8 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
534
546
 
535
547
  ### stack-set
536
548
 
549
+ Verde, diz quem escreve a crítica do plano: `critique.by: "session"` (a própria sessão, pelo checklist de grep da `morph-plan`) ou `"agent"` (degrau, mídia gerada ou tema sensível no plano, em `critique.reasons`). Com o gate do plano carimbado por política (chore, hotfix), o `plan.md` se edita até o primeiro `plan-check` verde e aí congela. Glob entre crases num cenário (`product/**/SKILL.md`) é o objeto da mudança, não artefato.
550
+
537
551
  **`morph-spec stack-set <stack> [--with <integrações>] [--json]`** — a stack que o usuário escolheu (`dotnet`, `nextjs` ou `dotnet+nextjs`) vai para `project.stack` do `config.json` (substitui, inclusive `unknown`); `--with neon,maf,docker` soma a `project.integrations`. Em seguida instala tudo o que a stack filtra, pelo mesmo caminho do `init`: rules de domínio em `.claude/rules/`, commands e skills. Tag desconhecida, `config.json` ausente ou ilegível, ou `docker` sem compose: exit 1 nomeando o motivo, nada gravado. A recomendação é sempre a stack completa do Morph, `dotnet+nextjs --with neon` (standard `architecture-stack-baseline`); recorte é escolha do usuário. Projeto vazio: o `init` pergunta (ou `init --stack <s> --with <i>`); sem terminal, a `morph-plan` pergunta antes de planejar.
538
552
 
539
553
  **Por quê:** as rules carregam no início de uma sessão do Claude Code (medido no 2.1.312: rule criada no meio da sessão não vale para os arquivos seguintes). Num projeto novo, só a stack declarada antes da sessão que implementa entrega as regras de domínio a tempo; o `plan-check`, o `verify` e o `update` só alcançam a sessão seguinte.
@@ -748,9 +762,8 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
748
762
  | Momento | Comando | Por quê |
749
763
  | --- | --- | --- |
750
764
  | Sessão que implementa | à vontade — filtro ou suíte —, pela fila `heavy` (`heavy.gateBash` ligado por padrão) | Implementar e conferir é dela; a prova do gate continua sendo o `verify` sem filtro |
751
- | Gate 3 | `verify {f} --ci --json` (sem filtro; sem CI disponível, roda local) | A prova tem de cobrir **todos** os nós; no CI ela não ocupa a máquina e roda em paralelo com o avaliador |
752
- | CI | a suíte inteira, todos os nós (`morph-verify.yml` roda o mesmo `verify {f}`) | idem — e o artefato dele é o stamp do Gate 3 |
753
- | PR, com `project.prChecks` | os workflows do projeto, esperados pelo `finish --pr` | a suíte que a PR já paga não roda de novo na máquina nem no `morph-verify.yml` |
765
+ | Gate 3 | `verify {f} --ci --json` (sem filtro; o que a PR prova fica para ela) | A prova tem de cobrir **todos** os nós, uma vez por SHA |
766
+ | CI da PR, com `project.prChecks` | os workflows do projeto, esperados pelo `finish --pr` | a suíte que a PR já paga não roda de novo na máquina |
754
767
  | Evidência de eval (cenário `Cn` com `prova: eval`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals`, sem stack e sem carimbo — é a evidência de runtime do cenário, não a verificação da feature |
755
768
 
756
769
  > **Nenhum relógio do `verify` é de 600 s.** Os 600 s que existem são a espera da fila
@@ -842,6 +855,8 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
842
855
 
843
856
  ### Refresh automático, worktree e testes no grafo
844
857
 
858
+ Grafo velho (a semente de uma worktree nasce no commit da default) roda um `graph refresh` incremental no lugar antes de recusar com `stale-graph`; `MORPH_GRAPH_AUTO_REFRESH=0` pula.
859
+
845
860
  **Refresh automático — só da raiz.** Depois de um comando de shell do agente (Bash ou PowerShell) que chama `git`, `gh` ou `morph-spec` (elo `graph-refresh-kick` do PostToolUse) e no início de cada sessão (SessionStart), o morph-spec solta um processo **destacado** (`.morph/framework/hooks/shared/graph-refresh.js`) que roda `graphify update` na **árvore principal** quando o grafo dela está velho. Quem dispara nunca espera.
846
861
 
847
862
  "Velho" é a regra única de `shared/graph-freshness.js`: o carimbo `built_at_commit` contra o HEAD e o registro da última rodada em `<git-common-dir>/morph-spec/graph-refresh-state.json`. O registro existe porque o `graphify update` do 0.9.50 **não recarimba** quando a topologia não muda ("outputs left untouched" — commit só de `.morph/`, corpo de método editado): uma rodada com exit 0 no HEAD H vale como grafo em dia em H, uma que falhou em H só se repete quando o HEAD andar, e graphify ausente fica registrado (o disparo não solta processo até ele voltar ao PATH). As regras do `.graphifyignore` entram por **conteúdo** — o registro guarda o hash das regras com que o grafo foi extraído; comentário, cabeçalho, BOM e CRLF não contam. Um lock por repositório (`graph-refresh.lock`, renovado a cada rodada) impede dois graphify no mesmo `graph.json`, a checagem é refeita depois de soltá-lo, e cada tentativa vai para `graph-refresh.log`. `MORPH_GRAPH_AUTO_REFRESH=0` desliga.
@@ -894,6 +909,6 @@ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit cod
894
909
  | `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
895
910
  | `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
896
911
  | `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`) |
897
- | `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.tests.runs[].{id,status,durationMs}` (qual suíte custou o quê), `reused[]`, `flaky[]`, `nodes.validators.{ran,skipped}` |
912
+ | `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.tests.runs[].{id,status,durationMs}` (qual suíte custou o quê), `reused[]`, `flaky[]`, `nodes.validators.{ran,skipped}` |
898
913
 
899
914
  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.
@@ -22,12 +22,12 @@ typechecks or runs tests: that is `morph-spec verify`.
22
22
  |-------|------|------|---------|
23
23
  | **SessionStart** | worktree-context.js | Context inject | Secondary worktree → feature identity (name/branch/phase) + missing-junction warning; primary root → active-feature summary (phase, slices done/total, pending gates), silent if none. Also seeds a worktree that has no codebase graph from the root's, and launches the background refresh of the ROOT's graph (`shared/graph-refresh.js`) |
24
24
  | **PreToolUse** (Write\|Edit) | _(native permissions.deny)_ | Block | Blocks edits to state.json and .morph/framework/ |
25
- | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to `plan.md` (and the uiux artifacts) after their gate is approved; an append-only `## Errata` block at the end of `plan.md` passes |
25
+ | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to `plan.md` (and the uiux artifacts) after their gate is approved; an append-only `## Errata` block at the end of `plan.md` passes. A plan gate stamped by policy (chore, hotfix) freezes only once plan-check wrote `checks.json` |
26
26
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
27
27
  | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
28
28
  | **PreToolUse** (Bash) | conventional-commit.js | Block | Blocks a `git commit -m` whose subject is not Conventional Commits (`<type>(<scope>): <subject>`); a `$(…)`/heredoc message, `-F` and git-generated `Merge`/`Revert` pass |
29
29
  | **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
30
- | **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
30
+ | **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass. Also blocks any `git` whose dir (cwd, `cd`, `-C`) is a `worktrees/<x>` folder without its `.git` file: git would walk up and act on the root's default branch; names `worktree remove --target` |
31
31
  | **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), bases a task tree on the default branch, prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
32
32
  | **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
33
33
  | **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (ADR, scenario/slice id, gate, plan.md), banner/`#region`, a block over 3 lines, or a doc-comment outside a published package. Old comments are never reported; last link of the chain |
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from 'fs';
2
+ import { existsSync, lstatSync } from 'fs';
3
3
  import { resolve, isAbsolute } from 'path';
4
4
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
5
5
  import { stateExists } from '../../shared/state-reader.js';
6
6
  import { scanForBlockers } from '../../shared/reparse-scan.js';
7
- import { tokenize } from '../../shared/command-tokens.js';
7
+ import { simpleCommands, tokenize } from '../../shared/command-tokens.js';
8
8
 
9
9
  // Generic recursive removals FOLLOW a worktree's junctions and empty the root's `.claude/` and `.morph/framework/`.
10
10
  // The reparse scan is the authority, not the path shape; anything unparseable passes.
@@ -12,8 +12,16 @@ import { tokenize } from '../../shared/command-tokens.js';
12
12
  // A hook never imports a sibling hook (its standalone tail would fire), so the tokenizer lives in shared/.
13
13
  export { tokenize };
14
14
 
15
+ // A flag belongs to its own simple command: the `-rn` of a grep after `&&` is not the `git rm`'s.
15
16
  export function extractRemovalTarget(command) {
16
- const tokens = tokenize(command);
17
+ for (const simple of simpleCommands(command)) {
18
+ const target = removalTargetOf(simple.map((t) => t.text));
19
+ if (target) return target;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function removalTargetOf(tokens) {
17
25
  if (!tokens.length) return null;
18
26
  const lower = tokens.map((t) => t.toLowerCase());
19
27
 
@@ -51,7 +59,7 @@ export async function run(payload) {
51
59
 
52
60
  const cwd = payload?.cwd || process.cwd();
53
61
  const target = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
54
- if (!existsSync(target)) return null;
62
+ if (!existsSync(target) || lstatSync(target).isFile()) return null;
55
63
 
56
64
  const { safe, blockers } = scanForBlockers(target);
57
65
  if (safe) return null;
@@ -1,20 +1,59 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync } from 'fs';
3
+ import { join, resolve } from 'path';
2
4
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
5
  import { stateExists } from '../../shared/state-reader.js';
4
6
  // From shared/, never from the sibling hook: importing a hook module fires its
5
7
  // standalone tail (stdin read + exit) inside THIS process.
6
- import { gitInvocations } from '../../shared/command-tokens.js';
8
+ import { gitInvocations, simpleCommands } from '../../shared/command-tokens.js';
9
+ import { WORKTREE_DIR_NAMES } from '../../shared/worktree-root.js';
10
+
11
+ const GIT_EXECUTABLE = /(^|[\\/])git(\.exe)?$/i;
12
+ const TREE_IN_CONTAINER = new RegExp(`^(.*)/(${WORKTREE_DIR_NAMES.map((n) => n.replace('.', '\\.')).join('|')})/([^/]+)(?:/|$)`);
7
13
 
8
14
  export function isRawWorktreeAdd(command) {
9
15
  return gitInvocations(command).some(({ sub, args }) => sub === 'worktree' && args[0]?.toLowerCase() === 'add');
10
16
  }
11
17
 
18
+ // A folder under worktrees/ whose `.git` file is gone is no worktree: git walks up and acts on the ROOT.
19
+ function orphanTreeOf(dir) {
20
+ const m = resolve(dir).replace(/\\/g, '/').match(TREE_IN_CONTAINER);
21
+ if (!m) return null;
22
+ const tree = `${m[1]}/${m[2]}/${m[3]}`;
23
+ return existsSync(tree) && !existsSync(join(tree, '.git')) && existsSync(join(m[1], '.git')) ? tree : null;
24
+ }
25
+
26
+ /** The orphan folder some `git` call of the command would run in (cwd, `cd`, `-C`), or null. */
27
+ export function gitInOrphanTree(command, cwd) {
28
+ let dir = cwd;
29
+ for (const tokens of simpleCommands(command)) {
30
+ const texts = tokens.map((t) => t.text);
31
+ if (texts[0] === 'cd' && texts[1]) { dir = resolve(dir, texts[1]); continue; }
32
+ const at = texts.findIndex((t) => GIT_EXECUTABLE.test(t));
33
+ if (at < 0) continue;
34
+ const c = texts.indexOf('-C', at);
35
+ const orphan = orphanTreeOf(c > at && texts[c + 1] ? resolve(dir, texts[c + 1]) : dir);
36
+ if (orphan) return orphan;
37
+ }
38
+ return null;
39
+ }
40
+
12
41
  export async function run(payload) {
13
42
  try {
14
- if (!stateExists()) return null;
15
-
16
43
  const command = payload?.tool_input?.command || '';
17
- if (!command || !isRawWorktreeAdd(command)) return null;
44
+ if (!command) return null;
45
+
46
+ // Before stateExists: the orphan folder has no `.morph/` of its own, and that is the case to stop.
47
+ const orphan = gitInOrphanTree(command, payload?.cwd || process.cwd());
48
+ if (orphan) {
49
+ return blockResult(
50
+ `Bloqueado: \`${orphan}\` não é mais uma worktree (o \`.git\` dela sumiu), então o git subiria até a raiz e rodaria na branch default.\n\n`
51
+ + `Limpe a pasta com \`morph-spec worktree remove --target "${orphan}"\` a partir da raiz (desmonta as junctions e apaga só o que está vazio). `
52
+ + 'Para trabalhar na feature, recrie a árvore com `morph-spec worktree setup <feature>`.',
53
+ );
54
+ }
55
+
56
+ if (!stateExists() || !isRawWorktreeAdd(command)) return null;
18
57
 
19
58
  return blockResult(
20
59
  'Bloqueado: `git worktree add` cru cria um worktree que o morph-spec NÃO enxerga — sem as junctions de infra ' +
@@ -3,7 +3,7 @@ import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
3
  import { stateExists } from '../../shared/state-reader.js';
4
4
  import { gitInvocations } from '../../shared/command-tokens.js';
5
5
 
6
- const CONVENTIONAL = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|style|revert)(\([^)]+\))?!?: \S/;
6
+ export const CONVENTIONAL = /^(feat|fix|refactor|test|chore|docs|perf|ci|build|style|revert)(\([^)]+\))?!?: \S/;
7
7
  const GIT_GENERATED = /^(Merge|Revert) /;
8
8
  const MESSAGE_FLAG = /^-[a-zA-Z]*m$/;
9
9
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { existsSync, readFileSync } from 'fs';
4
- import { stateExists, isGateApproved } from '../../shared/state-reader.js';
4
+ import { stateExists, isGateApproved, getFeature } from '../../shared/state-reader.js';
5
5
  import {
6
6
  isFeaturePath,
7
7
  extractFeatureName,
@@ -76,6 +76,13 @@ export async function run(payload) {
76
76
  return null;
77
77
  }
78
78
 
79
+ // A policy stamp is not a decision: that spec freezes once its checks file exists (written only with zero errors).
80
+ const gate = getFeature(featureName, root)?.approvalGates?.[requiredGate];
81
+ if (filename === 'plan.md' && String(gate?.approvedBy ?? '').startsWith('policy:')
82
+ && !existsSync(absPath.replace(/plan\.md$/, CHECKS_FILE))) {
83
+ return null;
84
+ }
85
+
79
86
  // The only way through is the append-only errata; any read or rebuild failure falls into the block.
80
87
  const toolName = getToolName(payload);
81
88
  let errataCheck = null;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync, existsSync, readdirSync } from 'fs';
4
- import { join, resolve } from 'path';
4
+ import { dirname, join, resolve } from 'path';
5
5
  import { execSync } from 'child_process';
6
6
  import {
7
7
  stateExists,
@@ -154,7 +154,11 @@ function buildWorktreeContext(dir, feature) {
154
154
 
155
155
  // The hook commands resolve through the .morph/framework junction: without it every guardrail here is silently off.
156
156
  function buildMissingInfraWarning(dir) {
157
+ // node_modules is linked only where the primary root has one (a .NET or monorepo root has none).
158
+ const common = resolveGitCommonDir(dir);
159
+ const root = common ? dirname(common) : null;
157
160
  const missing = ['node_modules', '.claude', join('.morph', 'framework')]
161
+ .filter((rel) => rel !== 'node_modules' || !root || existsSync(join(root, rel)))
158
162
  .filter((rel) => !existsSync(join(dir, rel)));
159
163
  if (!missing.length) return '';
160
164
  let text = `\n⚠ Infra compartilhada ausente (${missing.join(', ').replace(/\\/g, '/')}). Rode: morph-spec worktree link --all`;
@@ -96,7 +96,7 @@ const ASSIGNMENT = /^([A-Za-z_]\w*)=(.*)$/;
96
96
  const REDIRECT = /^(?:\d|&)?[<>]/;
97
97
  const SCRIPT_RUNNER = /^(?:python[\d.]*|py|node|bun|deno)$/;
98
98
 
99
- function simpleCommands(head) {
99
+ export function simpleCommands(head) {
100
100
  const commands = [];
101
101
  for (const segment of topLevelSegments(head).segments) {
102
102
  let current = [];
@@ -919,6 +919,22 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv,
919
919
  ? exec(spec.command, [...spec.args, '--last-failed'], { cwd: spec.cwd, timeout: specTimeout, env: composeEnv })
920
920
  : null;
921
921
 
922
+ // A spec the project declared in `e2e.quarantine` (out of the feature's scope, known red) is
923
+ // named, never hidden: it stops failing the node only when every red spec is one of them.
924
+ const stillRed = run.code !== 0 && retry?.code !== 0 ? failedSpecNames(retry?.output ?? run.output) : [];
925
+ const quarantine = (config?.e2e?.quarantine ?? []).filter((q) => typeof q === 'string' && q.trim());
926
+ if (stillRed.length > 0 && quarantine.length > 0 && stillRed.every((spec) => quarantine.some((q) => spec.includes(q)))) {
927
+ return {
928
+ status: 'pass',
929
+ quarantinedSpecs: stillRed,
930
+ flakySpecs: [],
931
+ command: commandLine,
932
+ output: '',
933
+ reason: joinNotes(`quarentena (e2e.quarantine): ${stillRed.join('; ')}`, notes),
934
+ count,
935
+ };
936
+ }
937
+
922
938
  if (run.code !== 0 && retry?.code !== 0) {
923
939
  return {
924
940
  status: 'fail',
@@ -161,14 +161,15 @@ function testKindOfTokens(tokens) {
161
161
  }
162
162
 
163
163
  const DOTNET_FILTERS = /^(--filter|--tests|--treenode-filter|\/testcasefilter:)/i;
164
+ const SCRIPT_FILTERS = /^(-Filter|--filter|-TestFilter)$/i;
164
165
  const NAME_FILTERS = /^(-t|-g|--grep|--testNamePattern|--test-name-pattern|--testPathPattern|--filter)(=|$)/;
165
166
  // A bare word may be a flag's value (`--reporter dot`); only a path-shaped argument selects files.
166
167
  const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
167
168
  const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
168
169
 
169
170
  function isFilteredRun(kind, tokens) {
170
- // A script's arguments are its own contract: no flag here proves it narrowed the run.
171
- if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return false;
171
+ // A test script's own filter parameter is the bench a plan declares (`test-pg.ps1 -Filter X`).
172
+ if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return tokens.some((t, i) => SCRIPT_FILTERS.test(t) && i + 1 < tokens.length);
172
173
  const runnerAt = executable(tokens[0]) === 'node'
173
174
  ? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
174
175
  : 0;
@@ -182,6 +182,9 @@ export function derivePhaseFrom(feature, hasArtifact) {
182
182
  // An approved gate advances the phase before the next artifact exists, or
183
183
  // that artifact could never be written.
184
184
  const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/mockups.md`);
185
+ // `unapprove` goes back through the normal flow: the revoked gate's phase holds until it is approved again.
186
+ const revoked = ['plan', 'uiux'].find((g) => gatesOf(feature)?.[g]?.revokedAt && gatesOf(feature)[g].approved !== true);
187
+ if (revoked && PHASE_INDEX[artifactPhase] > PHASE_INDEX[revoked]) return revoked;
185
188
  const implied = gateImpliedPhaseFrom(feature, designStarted);
186
189
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
187
190
  }
@@ -22,4 +22,4 @@ A memória da implementação depois de um `/compact` e o que o `next` lê para
22
22
  - `feito` exige evidência: o teste que prova, o `arquivo:linha` (`prova: leitura`) ou `evidencias/{Cn}.md` (`eval`/`manual`). `feito` sem evidência é o que o hook de Stop aponta.
23
23
  - `bloqueado` só com a pergunta em `## Notas`: o `next` devolve `decide` e ela vira `AskUserQuestion`. Respondida, a resposta vai em `## Notas` e o `Cn` volta a `pendente`.
24
24
  - `## Notas` é append-only, uma linha datada por decisão ou fato novo. Fato errado do plano vai na `## Errata` do `plan.md`, não aqui.
25
- - Atualize ao fechar cada `Cn` e commite junto (`wip(<f>): Cn …`).
25
+ - Atualize ao fechar cada `Cn` e commite junto (`feat(<f>): Cn …`, tipo do que o lote muda: `feat`, `fix`, `test`, `refactor`, `docs`, `chore`).
@@ -17,10 +17,11 @@ paths:
17
17
  - **`e2e` dispara a ação do dono até o efeito** (salvar, ativar, enviar): conferir que o controle aparece não prova. Dois estados (rascunho e ativo) são dois passos no mesmo `Cn`.
18
18
  - **Número que soma ou confere com outro** roda sobre dado com a forma do de produção; fixture feita à mão só tem as formas que você já imaginou.
19
19
  - **Observável**: que asserção, ou que passo no navegador, prova? Sem resposta, reescreva ou descarte.
20
- - **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório. Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`, que o parser não lê).
20
+ - **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório; o glob entre crases não conta (é o valor que a mudança trata, como o caminho que um CI filtra). Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`, que o parser não lê).
21
21
  - **Saída de LLM** prova-se por `eval` de runtime, nunca pelo texto do prompt: evidência em `3-implement/evidencias/{Cn}.md`; com nó `runner: "evals"` (standard `ai-agents-evals-with-cache`) o cenário mora no dataset.
22
22
  - **"A suíte roda de ponta a ponta"** prova-se pelo `verify {feature}`; nunca uma execução avulsa.
23
- - **Orçamento de teste:** um teste por cenário de risco; variantes são linhas de tabela (`[Theory]`/`it.each`); nenhum teste que só fixa texto, prompt, constante, DTO ou mapping; o mesmo comportamento numa camada só. Além disso é escopo para o revisor.
23
+ - **Orçamento de teste:** um teste por cenário de risco; variantes são linhas de tabela (`[Theory]`/`it.each`); nenhum teste que só fixa texto, prompt, constante, DTO ou mapping; o mesmo comportamento numa camada só. Além disso é escopo para o revisor. Esse teste conta no `code-budget` como exigido (40 linhas por cenário), e a prova de que ele morde é o `morph-spec prove-red`.
24
+ - **O risco do diff decide a cerimônia, não o tipo:** cenário de doc, config ou remoção é `leitura` e o Gate 3 é a revisão da própria sessão; um `Cn` com `risco:` põe lógica no diff e traz o verificador.
24
25
 
25
26
  ```markdown
26
27
  ## Cenários