@polymorphism-tech/morph-spec 10.7.0 → 10.8.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.
- package/README.md +2 -2
- package/bin/morph-spec.js +17 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +4 -2
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +32 -17
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +12 -4
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +43 -4
- package/framework/hooks/claude-code/core/conventional-commit.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +8 -1
- package/framework/hooks/claude-code/core/worktree-context.js +5 -1
- package/framework/hooks/shared/command-tokens.js +1 -1
- package/framework/hooks/shared/e2e-check.js +16 -0
- package/framework/hooks/shared/heavy-command.js +3 -2
- package/framework/hooks/shared/state-reader.js +3 -0
- package/framework/hooks/statusline.js +3 -1
- package/framework/rules/implement-report.md +1 -1
- package/framework/rules/plan-grammar.md +3 -2
- package/framework/schemas/config.schema.json +6 -1
- package/framework/schemas/feature.schema.json +4 -2
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +8 -7
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/review.md +9 -3
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/references/critique.md +26 -18
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/templates/context/ARCHITECTURE.md +0 -3
- package/package.json +1 -1
- package/src/commands/graph/diff.js +9 -2
- package/src/commands/project/worktree.js +86 -7
- package/src/commands/state/approve.js +4 -7
- package/src/commands/state/create.js +3 -3
- package/src/commands/state/finish.js +75 -4
- package/src/commands/state/gate-check.js +7 -2
- package/src/commands/state/report-check.js +33 -6
- package/src/commands/validation/plan-check.js +11 -2
- package/src/commands/validation/prove-red.js +95 -0
- package/src/commands/validation/verify.js +56 -40
- package/src/lib/ci/ci-verify.js +1 -106
- package/src/lib/ci/github-ci.js +5 -117
- package/src/lib/generators/architecture-scanner.js +1 -14
- package/src/lib/git/default-branch.js +28 -1
- package/src/lib/plan/artifact-vocabulary.js +7 -3
- package/src/lib/plan/critique.js +17 -0
- package/src/lib/review-confirmation.js +10 -11
- package/src/lib/review-reports.js +12 -3
- package/src/lib/risk/diff-risk.js +116 -0
- package/src/lib/risk-detector.js +1 -1
- package/src/lib/validators/code-budget/code-budget-validator.js +16 -4
- package/src/lib/validators/verify-runner.js +24 -2
- package/src/lib/verify/affected.js +3 -1
- package/src/lib/verify/detached.js +3 -0
- package/src/utils/refusal-line.js +37 -0
- 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.
|
|
6
|
+
**Version:** 10.8.0
|
|
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.
|
|
379
|
+
*morph-spec v10.8.0 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
|
|
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,8 +592,15 @@ 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;
|
|
602
|
+
const { refreshesFeatureView } = await import('../src/lib/verify/detached.js');
|
|
603
|
+
if (!refreshesFeatureView()) return;
|
|
588
604
|
const { refreshFeatureView } = await import('../src/lib/view/feature-view.js');
|
|
589
605
|
process.on('exit', () => refreshFeatureView(process.cwd(), feature));
|
|
590
606
|
});
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/MORPH.md
CHANGED
|
@@ -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
|
|
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): 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
|
|
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.
|
package/framework/agents.json
CHANGED
|
@@ -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",
|
package/framework/docs/CLI.md
CHANGED
|
@@ -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) 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
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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;
|
|
752
|
-
| CI
|
|
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
|
|
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
|
|
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
|
|
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
|
|
171
|
-
if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return
|
|
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
|
}
|
|
@@ -110,7 +110,9 @@ function readUserSettings(env = process.env) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function git(args, cwd) {
|
|
113
|
-
|
|
113
|
+
// --no-optional-locks: status/diff don't write the index, so they never take .git/index.lock
|
|
114
|
+
// nor leave it stale when the 3s timeout kills git on a large repo
|
|
115
|
+
return execFileSync('git', ['--no-optional-locks', ...args], { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, windowsHide: true }).toString('utf8').trim();
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// `-uno`: porcelain v2 lists untracked entries by default, which would change what the file count means.
|
|
@@ -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 (`
|
|
25
|
+
- Atualize ao fechar cada `Cn` e commite junto (`feat(<f>): Cn …`, tipo do que o lote muda: `feat`, `fix`, `test`, `refactor`, `docs`, `chore`).
|