@polymorphism-tech/morph-spec 9.6.0 → 9.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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +13 -6
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +3 -3
- package/framework/docs/CLI.md +53 -16
- package/framework/evals/review-checklist.md +5 -5
- package/framework/hooks/claude-code/core/README.md +4 -0
- package/framework/hooks/claude-code/core/test-budget.js +89 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/heavy-command.js +60 -1
- package/framework/hooks/shared/heavy-slots.js +41 -4
- package/framework/hooks/shared/host-services.js +433 -0
- package/framework/schemas/config.schema.json +54 -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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-review/SKILL.md +3 -2
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2 -1
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +14 -1
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +47 -24
- package/src/commands/observability/cost.js +12 -0
- package/src/commands/project/worktree.js +31 -5
- package/src/commands/state/advance.js +3 -0
- package/src/commands/state/create.js +3 -3
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +46 -2
- package/src/commands/state/report-check.js +36 -1
- package/src/commands/validation/plan-check.js +1 -0
- package/src/commands/validation/verify.js +3 -0
- package/src/lib/e2e/stack-teardown.js +19 -4
- package/src/lib/heavy/gate.js +22 -1
- package/src/lib/review-reports.js +23 -10
- package/src/lib/tasks/test-nodes.js +24 -6
- package/src/lib/tasks/test-runner.js +43 -1
- package/src/lib/telemetry/metrics.js +47 -3
- package/src/lib/validators/packages/package-validator.js +2 -2
- package/src/lib/validators/verify-runner.js +50 -12
- package/src/utils/agents-installer.js +5 -2
- package/src/utils/file-copier.js +26 -4
- package/src/utils/hooks-installer.js +4 -4
package/CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ This repo is the **framework source code** — not a project that uses morph-spe
|
|
|
13
13
|
```bash
|
|
14
14
|
# Once per clone: the dashboard render smoke test (test/dashboard/pages-smoke.test.js)
|
|
15
15
|
# needs the dashboard's own deps and FAILS without them — CI installs them too
|
|
16
|
-
|
|
16
|
+
cd src/dashboard && npm install
|
|
17
17
|
|
|
18
18
|
# Dev loop: only the test files the current change can reach (vs the merge-base with main)
|
|
19
19
|
npm run test:changed # add -- --list to see the selection and why
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 9.
|
|
6
|
+
**Version:** 9.8.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -380,4 +380,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
380
380
|
|
|
381
381
|
---
|
|
382
382
|
|
|
383
|
-
*morph-spec v9.
|
|
383
|
+
*morph-spec v9.8.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -344,7 +344,7 @@ program
|
|
|
344
344
|
.option('--filter <ids>', 'Run only the test nodes whose id matches (comma-separated; exact or substring). Unmatched nodes are reported as skip; matching nothing is a usage error (exit 2)')
|
|
345
345
|
.option('--linux', 'Run the test nodes inside a Linux container, on a COPY of the tree (the host tree is mounted read-only and copied in — a bind mount is case-insensitive and cannot reproduce case defects). No Docker = usage error, never a skip')
|
|
346
346
|
.option('--linux-image <img>', 'Image used by --linux (default: mcr.microsoft.com/dotnet/sdk:10.0)')
|
|
347
|
-
.option('--record-baseline', 'Measure every test node and write expectedCount back into .morph/config/config.json (only from an all-green run; never creates config.json)')
|
|
347
|
+
.option('--record-baseline', 'Measure every test node and write expectedCount of the node-test/evals nodes back into .morph/config/config.json, removing it from the other runners (only from an all-green run; never creates config.json)')
|
|
348
348
|
.option('--refresh-cache', 'Rewrite the response cache of every `runner: "evals"` node against the real provider. NEVER implicit: a cache rewrite is a paid call nobody asked for. Requires the declared envFile; no eval node declared, --skip-tests, or a --filter that excludes them all are usage errors (exit 2), refused BEFORE the cache is touched')
|
|
349
349
|
.option('--skip-tests', 'Skip the test node. The run is partial, so the verification stamp is NOT written (reason "skip-tests")')
|
|
350
350
|
.option('--skip-build', 'Skip the build node. The run is partial, so the verification stamp is NOT written (reason "skip-build")')
|
|
@@ -414,24 +414,31 @@ contextCommand
|
|
|
414
414
|
|
|
415
415
|
const e2eCommand = program
|
|
416
416
|
.command('e2e')
|
|
417
|
-
.description("Lifecycle of the feature's verification stack
|
|
417
|
+
.description("Lifecycle of the feature's verification stack: docker compose, then the host processes of e2e.hostServices")
|
|
418
|
+
.addHelpText('after', `
|
|
419
|
+
e2e.hostServices (.morph/config/config.json): apps that run as host processes instead of containers.
|
|
420
|
+
[{ "name", "start", "cwd": ".", "build"?, "env"?, "health"?, "timeoutMs": 180000 }] name and start required
|
|
421
|
+
Order on up and on the verify e2e node: compose up --wait -> each service in array order
|
|
422
|
+
(build -> start detached -> health) -> auth seed. \${VAR} in env/health comes from the stack env
|
|
423
|
+
(port block + e2e.auth.envFile) or process.env; an unset one refuses before any build.
|
|
424
|
+
They stay up until e2e down, finish, delete or worktree remove; e2e status --json lists them.`);
|
|
418
425
|
|
|
419
426
|
e2eCommand
|
|
420
427
|
.command('up <feature>')
|
|
421
|
-
.description('Bring the stack up
|
|
422
|
-
.option('--fresh', 'Recreate from scratch: down -v (named volumes included), drop the stamp, rebuild, re-seed auth')
|
|
428
|
+
.description('Bring the stack up, then e2e.hostServices, then the seed (reusing what still matches the working tree)')
|
|
429
|
+
.option('--fresh', 'Recreate from scratch: down -v (named volumes included), drop the stamp, rebuild and restart host services, re-seed auth')
|
|
423
430
|
.option('--json', 'Output as JSON')
|
|
424
431
|
.action((feature, options) => e2eUpCommand(feature, options));
|
|
425
432
|
|
|
426
433
|
e2eCommand
|
|
427
434
|
.command('status <feature>')
|
|
428
|
-
.description('Is the stack up, healthy, and built from the current source? Exit 0 only if all three')
|
|
435
|
+
.description('Is the stack up, healthy, and built from the current source? Exit 0 only if all three; --json adds hostServices [{name, pid, alive, health}]')
|
|
429
436
|
.option('--json', 'Output as JSON')
|
|
430
437
|
.action((feature, options) => e2eStatusCommand(feature, options));
|
|
431
438
|
|
|
432
439
|
e2eCommand
|
|
433
440
|
.command('down <feature>')
|
|
434
|
-
.description('Tear the stack down (best-effort — never blocks)')
|
|
441
|
+
.description('Tear the stack and the e2e.hostServices processes down (best-effort — never blocks)')
|
|
435
442
|
.option('--volumes', 'Also remove named volumes')
|
|
436
443
|
.option('--json', 'Output as JSON')
|
|
437
444
|
.action((feature, options) => e2eDownCommand(feature, options));
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/agents.json
CHANGED
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
],
|
|
368
368
|
"model": "sonnet",
|
|
369
369
|
"maxTurns": 90,
|
|
370
|
-
"persona": "Você é o verificador independente do Gate 3. Não participou da implementação. Sua missão é PROVAR cada cenário `C{n}` do `2-plan/checks.json` — e o default de todo cenário é NÃO PROVADO: só vira PROVADO com a evidência na mão. Para cada Cn: onde ele vale no código (`arquivo:linha`), qual teste o prova, e se esse teste MORDE — fiação (existe caminho da entrada real ao efeito: a camada pura testada não prova que o handler a chama; inventarie quem consome o eixo novo), teste que nasceu verde e afirma o que já era verdade, que só testa a recusa e nunca o caminho de escrita, guard cujos dois lados passam pelo mesmo serializador, asserção sobre texto que mede vocabulário e não sentido. Verificações mecânicas (.NET minimal API / Next): todo serviço injetado numa lambda de endpoint tem registro em `Program.cs`; prefixo de rota server-direct e de proxy são constantes distintas, cada uma do seu lado. Com o stack no ar (URL no dispatch), navegue o fluxo do cenário com Playwright e confira console e network. Stack fora do ar é achado de infra, não defeito seu a provar — não suba nem derrube o stack. Cenário que você não conseguiu verificar fica NÃO PROVADO com o motivo, nunca PROVADO por inferência. Siga a seção \"Verificador\" de `.morph/framework/evals/review-checklist.md` e grave `4-review/verifier.md` com `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, uma linha por cenário) — mais `## Checklist de tela`, `## Mídia gerada` e `## Mutações` quando o checklist pede. Cenário NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). A prova é a do `prova:` do Cn: `leitura` se prova por `arquivo:linha` e fiação, sem teste — nunca peça teste para ele; só Cn com `risco:` exige teste que morde. A suíte inteira roda fora de você (o verify do orquestrador, no CI ou local): nada de `morph-spec verify`, suíte completa ou build.
|
|
370
|
+
"persona": "Você é o verificador independente do Gate 3. Não participou da implementação. Sua missão é PROVAR cada cenário `C{n}` do `2-plan/checks.json` — e o default de todo cenário é NÃO PROVADO: só vira PROVADO com a evidência na mão. Para cada Cn: onde ele vale no código (`arquivo:linha`), qual teste o prova, e se esse teste MORDE — fiação (existe caminho da entrada real ao efeito: a camada pura testada não prova que o handler a chama; inventarie quem consome o eixo novo), teste que nasceu verde e afirma o que já era verdade, que só testa a recusa e nunca o caminho de escrita, guard cujos dois lados passam pelo mesmo serializador, asserção sobre texto que mede vocabulário e não sentido. Verificações mecânicas (.NET minimal API / Next): todo serviço injetado numa lambda de endpoint tem registro em `Program.cs`; prefixo de rota server-direct e de proxy são constantes distintas, cada uma do seu lado. Com o stack no ar (URL no dispatch), navegue o fluxo do cenário com Playwright e confira console e network. Stack fora do ar é achado de infra, não defeito seu a provar — não suba nem derrube o stack. Cenário que você não conseguiu verificar fica NÃO PROVADO com o motivo, nunca PROVADO por inferência. Siga a seção \"Verificador\" de `.morph/framework/evals/review-checklist.md` e grave `4-review/verifier.md` com `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, uma linha por cenário) — mais `## Checklist de tela`, `## Mídia gerada` e `## Mutações` quando o checklist pede. Cenário NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). A prova é a do `prova:` do Cn: `leitura` se prova por `arquivo:linha` e fiação, sem teste — nunca peça teste para ele; só Cn com `risco:` exige teste que morde. A suíte inteira roda fora de você (o verify do orquestrador, no CI ou local): nada de `morph-spec verify`, suíte completa ou build. Você não roda teste nem `verify` (o hook `test-budget` nega): a única execução que prova que um teste morde é no máximo UMA rodada de `morph-spec mutate {feature}` (até 3 mutantes, cada um com `expect`), o resto se prova por leitura — e só depois que o dispatch disser que revisor e segurança terminaram: eles leem o mesmo repositório. Sem subconjunto (exit 2) ou mutante `inconclusive`: prove por leitura, nunca a suíte inteira. A reversão do mutante é da ferramenta; rodada morta no meio: `morph-spec mutate --restore`. Arquivo você lê com Read, nunca `sed`/`cat`/`head`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`). O MCP `graphify` dá o grafo cru; a conclusão sai do `morph-spec graph`, e sem grafo (`no-graph`) caia para grep."
|
|
371
371
|
},
|
|
372
372
|
"reviewer": {
|
|
373
373
|
"title": "Gate 3 Reviewer",
|
|
@@ -398,7 +398,7 @@
|
|
|
398
398
|
],
|
|
399
399
|
"model": "sonnet",
|
|
400
400
|
"maxTurns": 60,
|
|
401
|
-
"persona": "Você é o revisor independente do Gate 3, read-only. Leia como o arquiteto que herda este repo amanhã: simples, legível, fácil de estender. Não participou da implementação e não prova cenários (é o verificador). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; `expectedCount` menor no `config.json` sem teste apagado no mesmo diff é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano — e o que o DIFF introduziu da lista fechada `architecture-engineering-lens#review-blockers`; o que já existia no arquivo é nota); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
|
|
401
|
+
"persona": "Você é o revisor independente do Gate 3, read-only. Leia como o arquiteto que herda este repo amanhã: simples, legível, fácil de estender. Não participou da implementação e não prova cenários (é o verificador). Não roda teste nem `verify` (o hook `test-budget` nega). Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Julga o diff em cinco dimensões, na seção \"Revisor\" de `.morph/framework/evals/review-checklist.md`: ESCOPO (o diff contra as fatias e o `## Contrato de implementação` do plano — arquivo fora de toda fatia, comportamento que nenhum cenário pediu, teste além do orçamento — teste para Cn `prova: leitura`, mais de um por Cn de risco sem porquê no plano, teste que só fixa texto/constante/DTO; teste apagado sem porquê no relatório da fatia, edição de `.sln`/`.csproj`/glob do runner que tira testes da execução, ou `expectedCount` menor no `config.json` sem teste apagado no mesmo diff, é `regressao`); ARQUITETURA (padrão ou abstração nova contra o stack do projeto e os standards — camada que o projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano — e o que o DIFF introduziu da lista fechada `architecture-engineering-lens#review-blockers`; o que já existia no arquivo é nota); REGRESSÃO (para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`; sem grafo — `no-graph`, `not-found`, `ambiguous` — caia para `git grep -n <símbolo> {sha}` e siga, a ausência de grafo nunca bloqueia; ausência de consumidor nunca sai do grafo); O QUE A CORREÇÃO ABRIU (revisando uma correção de rodada anterior, releia o comportamento vizinho); PERFORMANCE só com evidência (lista sem paginação no servidor sobre volume real, consulta N+1 visível no diff) e RASTREABILIDADE de chamada a LLM (dá para reconstruir o que foi ao provedor e o que voltou?). Tipos de bloqueador: `escopo`, `arquitetura`, `regressao`, `performance`. Seu default é SHIP: dúvida vira nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
|
|
402
402
|
},
|
|
403
403
|
"security": {
|
|
404
404
|
"title": "Gate 3 Security Reviewer",
|
|
@@ -425,7 +425,7 @@
|
|
|
425
425
|
"mcp": [],
|
|
426
426
|
"model": "opus",
|
|
427
427
|
"maxTurns": 60,
|
|
428
|
-
"persona": "Você é a revisão de segurança do Gate 3, read-only, despachada só quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration. Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Procure, na seção \"Segurança\" de `.morph/framework/evals/review-checklist.md`: authz faltando ou checada do lado errado, segredo no código, no log ou no cliente, injeção (SQL, comando, prompt), dado pessoal vazando para log, resposta ou provedor, custo sem teto (chamada paga em laço, sem limite por usuário), mídia gerada publicada sem revisão humana, migration destrutiva (DROP/rename de coluna com dado, branch Neon de produção). Bloqueador é tipo `seguranca` e exige o CAMINHO DE EXPLORAÇÃO: quem, por qual entrada, chega a quê. Sem caminho, é nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
|
|
428
|
+
"persona": "Você é a revisão de segurança do Gate 3, read-only, despachada só quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration. Você lê SÓ o commit revisado: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, `git log {base}..{sha}` — nunca a árvore de trabalho (Read/Glob/Grep sobre `src/` leem o disco, onde o verificador pode estar com um mutante aplicado). `base` e `sha` vêm no dispatch. Não roda teste nem `verify` (o hook `test-budget` nega). Procure, na seção \"Segurança\" de `.morph/framework/evals/review-checklist.md`: authz faltando ou checada do lado errado, segredo no código, no log ou no cliente, injeção (SQL, comando, prompt), dado pessoal vazando para log, resposta ou provedor, custo sem teto (chamada paga em laço, sem limite por usuário), mídia gerada publicada sem revisão humana, migration destrutiva (DROP/rename de coluna com dado, branch Neon de produção). Bloqueador é tipo `seguranca` e exige o CAMINHO DE EXPLORAÇÃO: quem, por qual entrada, chega a quê. Sem caminho, é nota. Arquivo do framework ou da feature (o checklist, o plan.md) você lê com Read, nunca `sed`/`cat`/`head`; código do commit, com `git show {sha}:<caminho>`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`)."
|
|
429
429
|
},
|
|
430
430
|
"scout": {
|
|
431
431
|
"title": "Codebase Scout",
|
package/framework/docs/CLI.md
CHANGED
|
@@ -108,7 +108,7 @@ Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma
|
|
|
108
108
|
|
|
109
109
|
### delete
|
|
110
110
|
|
|
111
|
-
**`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
|
|
111
|
+
**`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto). A stack e2e da feature cai como no `finish`: `down --remove-orphans --rmi local -v`, mais os `e2e.hostServices`.
|
|
112
112
|
|
|
113
113
|
### retype
|
|
114
114
|
|
|
@@ -124,7 +124,7 @@ Feature sem `workType` (pré-v8) é tratada como `feature`.
|
|
|
124
124
|
|
|
125
125
|
**Í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`.
|
|
126
126
|
|
|
127
|
-
`--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Saída de teste não rastreada (`test-results/`, `TestResults/`, `playwright-report/`, `blob-report/`) não conta como árvore suja: na raiz só é tolerada, na worktree sai junto com o desmonte; qualquer outro arquivo não commitado recusa. Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas;
|
|
127
|
+
`--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Saída de teste não rastreada (`test-results/`, `TestResults/`, `playwright-report/`, `blob-report/`) não conta como árvore suja: na raiz só é tolerada, na worktree sai junto com o desmonte; qualquer outro arquivo não commitado recusa. Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; derruba a stack e2e com `down --remove-orphans --rmi local -v` (`--keep-volumes` tira o `-v`) e os `e2e.hostServices` — `--rmi local` leva só as imagens que o compose construiu, nunca a puxada (`postgres`), que a próxima feature baixaria de novo; `docker builder prune` nunca roda (o cache é global da máquina). No `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`.
|
|
128
128
|
|
|
129
129
|
**Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. O `--dry-run` já avisa (`teardownDeferred` no pré-voo).
|
|
130
130
|
|
|
@@ -164,7 +164,7 @@ O `.morph/state.json` do worktree é índice derivável: ele se refaz sozinho a
|
|
|
164
164
|
|
|
165
165
|
**`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho.
|
|
166
166
|
|
|
167
|
-
Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e
|
|
167
|
+
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, e o `worktree reap` herda por passar por aqui). 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).
|
|
168
168
|
|
|
169
169
|
**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.
|
|
170
170
|
|
|
@@ -280,7 +280,7 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
|
|
|
280
280
|
|
|
281
281
|
**`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira.
|
|
282
282
|
|
|
283
|
-
O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build).
|
|
283
|
+
O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável, sobe os `e2e.hostServices` (ver `e2e` abaixo) e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build).
|
|
284
284
|
|
|
285
285
|
**Onde o nó `build` compila** (chaves de `project` em `.morph/config/config.json`, forma em `config.schema.json`): `project.stack` (tags unidas por `+`, ex. `dotnet+nextjs`) escolhe os passos; `project.backendPath` é a pasta da solução .NET e `project.frontendPath` a do frontend (`tsc --noEmit` e, com `eslint.config.*` e o pacote `eslint` instalados, `eslint --quiet` só nos arquivos que a feature mudou contra a base), ambas relativas à raiz. Sem `backendPath` e sem `.sln`/`.slnx`/`.csproj` na raiz, o build compila a **única** `.sln`/`.slnx` do repositório (senão o único `.csproj`), fora de `worktrees/`, `node_modules/`, `bin/` e `obj/` (`discoverDotnetSolution`, `test-runner.js`); nenhuma ou várias → o build falha e o `feedback[]` lista as candidatas e manda declarar `project.backendPath`.
|
|
286
286
|
|
|
@@ -290,13 +290,15 @@ As specs são procuradas na raiz, em `project.frontendPath` e, sem ele, nas past
|
|
|
290
290
|
|
|
291
291
|
**Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
|
|
292
292
|
|
|
293
|
-
**Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **
|
|
293
|
+
**Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `timeoutMs` do nó > `project.testTimeoutMs` no `config.json` > **30 min por projeto de teste encadeado** (o build também tem teto de 30 min) — uma suíte longa nunca nasce condenada pelo relógio. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
**Falha nomeada:** num nó de testes vermelho, o `feedback[]` traz primeiro `[tests/{id}] falhou: <teste>` (até 10, lidas da saída **inteira**: `not ok` do TAP do `node --test` — só as folhas, a suíte-mãe sai —, `FAIL`/`×` do vitest, `failed`/`Failed`/`com falha` do dotnet), depois `[tests/{id}] log completo: .morph/logs/verify-tests-{id}.log` e só então a cauda de 20 linhas; o `reason` do nó resume as três primeiras. Runner que nenhuma forma reconhece fica só com o caminho do log e a cauda. O log é gravado a cada run vermelho (e no `--linux`); erro de escrita não muda o veredito.
|
|
296
|
+
|
|
297
|
+
**`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build`, reconstrói e reinicia os `e2e.hostServices` e força o seed de auth a rodar de novo.
|
|
296
298
|
|
|
297
299
|
**`--filter <ids>` escolhe os nós** (lista separada por vírgula, casando `id` exato ou substring). O Gate 3 é `verify {f} --json` **sem filtro**, porque a prova que o verificador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar.
|
|
298
300
|
|
|
299
|
-
**`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não.
|
|
301
|
+
**`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (e apaga o dos demais) (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não.
|
|
300
302
|
|
|
301
303
|
**`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado.
|
|
302
304
|
|
|
@@ -364,11 +366,36 @@ Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falh
|
|
|
364
366
|
|
|
365
367
|
**`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo.
|
|
366
368
|
|
|
367
|
-
`--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed.
|
|
369
|
+
`--fresh` derruba com volumes (`down -v`), apaga o carimbo, reconstrói e reinicia os `e2e.hostServices` e re-roda o seed.
|
|
368
370
|
|
|
369
371
|
`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`.
|
|
370
372
|
|
|
371
|
-
`down [--volumes]` é best-effort e nunca bloqueia.
|
|
373
|
+
`down [--volumes]` é best-effort e nunca bloqueia; derruba também os `e2e.hostServices`.
|
|
374
|
+
|
|
375
|
+
**Serviços no host (`e2e.hostServices`).** App cujo build é caro dentro da VM do Docker roda como processo do host, nas portas do bloco, e o compose fica só com as dependências. Declaração em `.morph/config/config.json` (forma em `config.schema.json`):
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
"e2e": { "hostServices": [
|
|
379
|
+
{ "name": "api", "cwd": "src/Api", "build": "dotnet build -c Debug",
|
|
380
|
+
"start": "dotnet bin/Debug/net10.0/Api.dll",
|
|
381
|
+
"env": { "ASPNETCORE_URLS": "http://localhost:${API_PORT}" },
|
|
382
|
+
"health": "http://localhost:${API_PORT}/health" }
|
|
383
|
+
] }
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
| Campo | Obrigatório | Significado |
|
|
387
|
+
| --- | --- | --- |
|
|
388
|
+
| `name` | sim | `[A-Za-z0-9._-]`, único no array; nomeia o log e as mensagens |
|
|
389
|
+
| `start` | sim | Comando do processo, iniciado destacado; saída em `<git-common-dir>/morph-spec/e2e-host-<projeto>-<name>.log` |
|
|
390
|
+
| `cwd` | não (`.`) | Relativo à raiz do projeto; inexistente recusa |
|
|
391
|
+
| `build` | não | Roda antes do `start`; exit ≠ 0 é falha |
|
|
392
|
+
| `env` | não | Somado ao env da stack (bloco de portas + `e2e.auth.envFile`) e ao `process.env` |
|
|
393
|
+
| `health` | não | URL; verde = resposta HTTP < 500. Sem ela, processo vivo 2 s depois do `start` conta como pronto |
|
|
394
|
+
| `timeoutMs` | não (180 000) | Limita build + start + health **juntos** |
|
|
395
|
+
|
|
396
|
+
`${VAR}` em `env` e `health` vem do env da stack, depois do `process.env`; variável sem valor (ou vazia) recusa **antes** do build, nomeando a variável e o serviço.
|
|
397
|
+
|
|
398
|
+
**Ordem** (`bringUpE2e`, a mesma no `e2e up` e no nó `e2e` do `verify`): `compose up --wait` → cada serviço na ordem do array (build → start → health) → seed (`e2e.auth`) → specs (só no verify). **Reuso:** PIDs, hora de criação e carimbo do código (o mesmo `HEAD` + status + diff da stack) ficam em `<git-common-dir>/morph-spec/e2e-host-<projeto>.json`; carimbo igual e health verde → `reused`; carimbo divergente, conjunto de serviços mudado ou `--fresh`/`--fresh-e2e` → todos são mortos, reconstruídos e reiniciados; health morto com carimbo igual → só aquele reinicia. **Falha** (build ≠ 0, timeout, processo que morre, health que nunca fica verde) → `e2e up` sai 1 e o nó `e2e` falha, com o nome do serviço e a cauda do log; os serviços subidos nesta rodada são mortos e o seed não roda. Os serviços **ficam no ar** depois do `up` e do `verify`, até `e2e down`, `finish`, `delete` ou `worktree remove`. Um processo só é morto se a hora de criação do PID vivo bate com a gravada: PID reciclado não é morto, só avisado. `e2e status --json` traz `hostServices: [{ name, pid, alive, health }]`.
|
|
372
399
|
|
|
373
400
|
Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada.
|
|
374
401
|
|
|
@@ -384,6 +411,8 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
|
|
|
384
411
|
|
|
385
412
|
**Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo do piso do tipo do comando, espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo).
|
|
386
413
|
|
|
414
|
+
**Uma suíte de `verify` por vez:** o nó de testes de um `verify` espera também enquanto o `verify` de outra feature está no nó de testes, segurando o slot que já tinha; `heavy run` (o filtro de sub-agente), build, e2e e `mutate` não esperam por isso, só pelos slots. Estouro da espera sai `reason: "suite"` (exit 3).
|
|
415
|
+
|
|
387
416
|
**Piso de RAM por tipo** (`heavy.minFreeMemGbByKind`, padrão `unit` 1,5 · `build` 4 · `e2e` 4 GB): `unit` = vitest/jest, `npm|pnpm|yarn test`, `npm run test[:x]` e o nó `tests` do verify; `e2e` = playwright, `docker compose up`, script com `e2e` no nome, o nó `e2e` e o `e2e up`; `build` = o resto (`dotnet build|test|publish` — o `dotnet test` compila —, `next build`, `docker build`, `mutate` e comando desconhecido). O `heavy run` e o hook do `gateBash` classificam pelo comando; o `verify` toma o slot com o piso do primeiro nó e, antes de um nó mais pesado (tests → e2e), espera de novo só pela RAM segurando o slot — estourar ali também é exit 3, sem stamp. `heavy.minFreeMemGb` (4) virou o fallback: **definido explicitamente**, vale para todo tipo que `minFreeMemGbByKind` não lista (`0` continua desligando a checagem em tudo); sem ele, cada tipo usa o seu padrão.
|
|
388
417
|
|
|
389
418
|
**Ocupado por RAM** (`verify`, `e2e up`, `heavy run`): a mensagem nomeia o piso e a chave (`heavy.minFreeMemGbByKind.<tipo>`) e lista as stacks e2e de pé deste repo (`docker compose ls`, prefixo `morph-`, compose sob a raiz principal — de todas as worktrees). Feature já arquivada (`.morph/archive/<f>` sem `.morph/features/<f>`) vem com `morph-spec e2e down <f>`; stack sem feature, com `docker compose -p <nome> down`. Docker fora ou lento (10 s) = lista vazia, nunca erro.
|
|
@@ -410,6 +439,8 @@ Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda
|
|
|
410
439
|
|
|
411
440
|
`run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula PROCESSOS do contrato de dispatch (`heavy run`) segue valendo para eles.
|
|
412
441
|
|
|
442
|
+
**Teto de teste do sub-agent (`test-budget`, sempre ligado, antes do `heavy-gate`):** só Bash com `agent_id` (o orquestrador nunca é contado) e com `.morph/` presente. Rodada = comando de teste (`testRunKind`: `dotnet test|vstest`, `vitest`, `jest`, `playwright test`, `npm|pnpm|yarn|bun test`, inclusive dentro de `heavy run --`) ou `morph-spec verify`; listagem (`--list`) não conta. Nega `verify` sem `--filter` (é do orquestrador), a 4ª rodada do mesmo sub-agent (`verify --filter` conta; a que sai com exit 3, fila cheia, não) com a instrução de gravar `PARCIAL` e devolver (a retomada pelo orquestrador, por `SendMessage` com o id do agente, devolve as 3 rodadas), e qualquer teste ou `verify` de `morph-verifier`/`morph-reviewer`/`morph-security` — `morph-spec mutate` passa. O contador mora em `.morph/memory/test-runs/<agent_id>.json`, gravado no `PostToolUse` e no `PostToolUseFailure`; qualquer erro é fail-open.
|
|
443
|
+
|
|
413
444
|
**Medido** com `claude -p` real e a chain instalada: fila cheia recusa após a espera e não mexe na vaga alheia; a vaga está presa durante a chamada e volta vazia depois, com exit 0 e com exit 1. Custo num Bash **leve**: ~1,5 ms na chain Pre (mediana 61,4 → 62,9 ms, medido intercalado) e ~0 na Post; os módulos do semáforo só carregam quando o comando é pesado.
|
|
414
445
|
|
|
415
446
|
**Por quê.** RAM baixa espera **com ou sem** holder de slot: a pressão costuma vir de fora (vmmem do WSL, IDE), e "sem holder → roda" reproduziria o "roda e morre". Com `heavy.enabled: false` nunca roda: sem semáforo, nada prova que outra sessão não está compilando. O núcleo (`heavy-config`, `heavy-holders`, `heavy-slots`) mora em `framework/hooks/shared`, porque hook não importa `src/`.
|
|
@@ -523,9 +554,9 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
|
|
|
523
554
|
"tests": [
|
|
524
555
|
{ "id": "dotnet", "runner": "dotnet", "cwd": ".",
|
|
525
556
|
"command": "dotnet test tests/App.Tests/App.Tests.csproj --nologo -v q --no-build",
|
|
526
|
-
"build": ["tests/App.Tests/App.Tests.csproj"], "
|
|
557
|
+
"build": ["tests/App.Tests/App.Tests.csproj"], "timeoutMs": 600000 },
|
|
527
558
|
{ "id": "vitest", "runner": "vitest", "cwd": "catalog-admin",
|
|
528
|
-
"command": "npx vitest run --reporter=default"
|
|
559
|
+
"command": "npx vitest run --reporter=default" },
|
|
529
560
|
{ "id": "api-integration", "runner": "dotnet", "cwd": ".",
|
|
530
561
|
"command": "dotnet test tests/App.Integration.Tests/App.Integration.Tests.csproj -v q", "e2e": true }
|
|
531
562
|
]
|
|
@@ -539,8 +570,8 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
|
|
|
539
570
|
| `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
|
|
540
571
|
| `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
|
|
541
572
|
| `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
|
|
542
|
-
| `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
|
|
543
|
-
| `timeoutMs` | não | Teto deste nó.
|
|
573
|
+
| `expectedCount` | não | Baseline, só nos runners de contagem estável (`node-test`, `evals`); nos outros é ignorada e o `--record-baseline` a apaga. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
|
|
574
|
+
| `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
|
|
544
575
|
| `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). O `mutate` não o exige: sem ele, descobre o alvo pelo comando. Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
|
|
545
576
|
| `e2e` | não (`false`) | Exige a stack de pé. No host roda **depois** do nó `e2e`, contra a stack que ele subiu e com o mesmo env do compose (portas, `PLAYWRIGHT_BASE_URL`, `COMPOSE_PROJECT_NAME`); stack que não subiu (`e2e` em `skip`/`fail`) pula o nó com o motivo e um aviso `[tests/{id}] warning:`. Pulado **com motivo** dentro do `--linux`. Playwright não entra aqui: é o próprio nó `e2e` (detectado ou `e2e.command`), e declará-lo também como nó de teste rodaria a suíte duas vezes |
|
|
546
577
|
| `linuxImage` | não | Imagem do container deste nó sob `--linux` |
|
|
@@ -677,6 +708,11 @@ reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa**
|
|
|
677
708
|
— um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
|
|
678
709
|
nó verde**, que é o único lugar onde ele pode existir.
|
|
679
710
|
|
|
711
|
+
**Baseline só onde a contagem fica parada.** `dotnet`, `vitest`, `jest` e `playwright` crescem a cada
|
|
712
|
+
fatia: a baseline deles só avisava a alta ou barrava a limpeza deliberada, e teste apagado já aparece no
|
|
713
|
+
diff que o Gate 3 lê. Nesses runners não há baseline; vale o **piso zero** — nó verde que executou `0`
|
|
714
|
+
testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou de rodar).
|
|
715
|
+
|
|
680
716
|
**Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
|
|
681
717
|
os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
|
|
682
718
|
antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
|
|
@@ -689,10 +725,10 @@ a partir do binário do último build bom e reportava verde. `--no-build` contin
|
|
|
689
725
|
sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógio do caminho quente.
|
|
690
726
|
|
|
691
727
|
**O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
|
|
692
|
-
escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
|
|
728
|
+
escrita: mede cada nó numa execução real e grava `expectedCount` dos nós `node-test`/`evals` de volta no `config.json` (apaga o dos demais, listados como `removed`; escrita
|
|
693
729
|
atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
|
|
694
730
|
vermelha (fora o `count-drop` com exit 0, abaixo), execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
|
|
695
|
-
afirmação sobre a outra metade que ninguém fez), contagem desconhecida
|
|
731
|
+
afirmação sobre a outra metade que ninguém fez), contagem desconhecida num nó `node-test`/`evals`, e **sem `config.json` ou sem
|
|
696
732
|
`project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
|
|
697
733
|
fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
|
|
698
734
|
recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
|
|
@@ -712,7 +748,7 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
|
|
|
712
748
|
|
|
713
749
|
> **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
|
|
714
750
|
> corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
|
|
715
|
-
> relógios que o CLI de fato tem: build **
|
|
751
|
+
> relógios que o CLI de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
|
|
716
752
|
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
|
|
717
753
|
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
718
754
|
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
|
|
@@ -823,6 +859,7 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
823
859
|
| rerun | o mesmo comando **produtor** (antes do primeiro `\|` fora de aspas, sem `2>&1`) na mesma feature até 10 min depois do anterior; `rate` conta as repetições, `burstRate` toda execução que cai numa rajada, a primeira incluída. `rerunClasses`: `fix-loop` (falhou, houve edição), `flake` (falhou, sem edição, passou), `retry` (falhou de novo sem edição), `re-verify` (passou, houve edição), `redundant` (passou, sem edição), `timeout`, `refilter` (mesmo produtor, outro filtro de saída, sem edição: rodou de novo para ver outras linhas), `unknown-after-edit`/`unknown-no-edit` (exit mascarado por pipe ou evento v1) |
|
|
824
860
|
| releitura | bytes da mesma janela (arquivo + `offset`/`limit`) lida de novo na mesma feature, com os arquivos de maior releitura; `windowed` conta as leituras com `bytesRead`. As outras (v1) contam o arquivo inteiro, e a taxa vira um teto |
|
|
825
861
|
| verify de feature | `morph-spec verify <f>` executado (fora de aspas e de heredoc) por feature, com a mediana; `verify.unchangedRepeats` conta, por feature, o evento `Verify` cujo `fingerprint` é igual ao do anterior |
|
|
862
|
+
| teste fora do verify | `testOutsideVerify`: sub-agente rodando `morph-spec verify` sem `--filter`, rodadas de teste de implementador da 3ª em diante (por agente) e teste/verify de agente de review, em execuções e minutos sobre o total cronometrado; exit 3 (fila cheia) não conta. Review por persona `morph-*` aparece à parte: é o que o hook `test-budget` nega |
|
|
826
863
|
| suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
|
|
827
864
|
| git | ocorrências de `git status` e `git diff` na janela |
|
|
828
865
|
| reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
|
|
@@ -71,8 +71,8 @@ sem linha ou não PROVADO. Numa confirmação, uma `## Checks` nova só com os C
|
|
|
71
71
|
5. **Runtime**: com o stack no ar (URL no dispatch), navegue o fluxo do cenário e confira console e
|
|
72
72
|
network. Stack fora do ar é achado de infra, não defeito seu a provar.
|
|
73
73
|
|
|
74
|
-
A suíte inteira é do orquestrador. Você roda
|
|
75
|
-
|
|
74
|
+
A suíte inteira é do orquestrador. Você não roda teste nem `verify` (o hook `test-budget` nega): a
|
|
75
|
+
execução que prova mordida é a mutação abaixo; o resto, por leitura.
|
|
76
76
|
|
|
77
77
|
**Prova por mutação — opcional, no máximo 3, só depois que revisor e segurança terminaram.** Só
|
|
78
78
|
quando o diff tem uma linha de guarda ou regra que um cenário exige e você não achou o teste que a
|
|
@@ -116,9 +116,9 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
|
|
|
116
116
|
1. **Escopo**: o diff contra as fatias (`arquivos:`) e o `## Contrato de implementação` do plano.
|
|
117
117
|
Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
|
|
118
118
|
risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
|
|
119
|
-
camadas — bloqueador `escopo` (apague, não acrescente).
|
|
120
|
-
`.morph/config/config.json` do diff sem teste apagado no mesmo
|
|
121
|
-
suíte que parou de rodar, não limpeza.
|
|
119
|
+
camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê no relatório da
|
|
120
|
+
fatia, edição de `.sln`/`.csproj`/glob do runner que tira testes da execução, ou `expectedCount` menor no `.morph/config/config.json` do diff sem teste apagado no mesmo
|
|
121
|
+
diff, é bloqueador `regressao`: é suíte que parou de rodar, não limpeza.
|
|
122
122
|
2. **Arquitetura**: padrão ou abstração nova contra o stack do projeto e os standards (camada que o
|
|
123
123
|
projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano).
|
|
124
124
|
3. **Regressão**: para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
# hooks/claude-code/core
|
|
2
2
|
Part of morph-spec NLH.
|
|
3
3
|
See `framework/MORPH.md` for full documentation.
|
|
4
|
+
|
|
5
|
+
## test-budget
|
|
6
|
+
|
|
7
|
+
`PreToolUse` Bash (antes do `heavy-gate`), `PostToolUse` e `PostToolUseFailure`. Só age em sub-agent (`agent_id`) com `.morph/` presente. Nega: `morph-spec verify` sem `--filter`; a 4ª rodada de teste do mesmo sub-agent (rodada = `testRunKind` ou `verify --filter`; exit 3 não conta), mandando gravar `PARCIAL` e devolver; todo teste ou `verify` de `morph-verifier`/`morph-reviewer`/`morph-security` (`morph-spec mutate` passa). Contador em `.morph/memory/test-runs/<agent_id>.json`; o `SendMessage` do orquestrador para o agente (a retomada depois do `PARCIAL`) zera o dele. Fail-open.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
5
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
6
|
+
import { QUEUE_FULL_EXIT, REVIEW_AGENT_TYPES, morphVerifyCall, testRunKind } from '../../shared/heavy-command.js';
|
|
7
|
+
|
|
8
|
+
export const TEST_RUN_BUDGET = 3;
|
|
9
|
+
|
|
10
|
+
export const REASONS = {
|
|
11
|
+
verify: '`morph-spec verify` cheio é do orquestrador, depois do seu retorno. Rode só o filtro dos seus testes.',
|
|
12
|
+
budget: '4ª rodada de teste nesta fatia. Grave `PARCIAL` na 1ª linha do relatório com o que ainda falha e devolva: o orquestrador decide.',
|
|
13
|
+
review: 'Agente de review não roda teste: a prova é o verify do orquestrador. Para provar que um teste morde, `morph-spec mutate`.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Pre and Post must land on the same counter: a sub-agent's cwd drifts into subfolders.
|
|
17
|
+
const projectRoot = (payload, env) => env.CLAUDE_PROJECT_DIR || payload?.cwd || process.cwd();
|
|
18
|
+
|
|
19
|
+
const counterPath = (root, agentId) =>
|
|
20
|
+
join(root, '.morph', 'memory', 'test-runs', `${String(agentId).replace(/[^A-Za-z0-9_-]/g, '_')}.json`);
|
|
21
|
+
|
|
22
|
+
function readCounter(path) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(path, 'utf-8')) ?? {};
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const readRuns = (path) => Number(readCounter(path).runs) || 0;
|
|
31
|
+
|
|
32
|
+
function recordRun(path, name) {
|
|
33
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
34
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
35
|
+
writeFileSync(tmp, JSON.stringify({ runs: readRuns(path) + 1, ...(name ? { name } : {}) }));
|
|
36
|
+
renameSync(tmp, path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function exitCodeOf(payload) {
|
|
40
|
+
const code = /^exit code (\d+)/i.exec(String(payload.error ?? '').split('\n', 1)[0]);
|
|
41
|
+
return code ? Number(code[1]) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The orchestrator resuming an agent is the decision the budget hands back to it: a fresh budget.
|
|
45
|
+
function resetOnResume(payload, env) {
|
|
46
|
+
const to = payload.tool_input?.to;
|
|
47
|
+
if (payload.agent_id || payload.hook_event_name !== 'PostToolUse' || !to) return;
|
|
48
|
+
const root = projectRoot(payload, env);
|
|
49
|
+
if (!stateExists(root)) return;
|
|
50
|
+
// `to` may be the dispatch name instead of the id: the counter records the name it ran under.
|
|
51
|
+
const byId = counterPath(root, to);
|
|
52
|
+
const dir = join(byId, '..');
|
|
53
|
+
for (const file of readdirSync(dir)) {
|
|
54
|
+
const path = join(dir, file);
|
|
55
|
+
if (path === byId || readCounter(path).name === to) rmSync(path, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function run(payload, deps = {}) {
|
|
60
|
+
try {
|
|
61
|
+
if (payload?.tool_name === 'SendMessage') {
|
|
62
|
+
resetOnResume(payload, deps.env ?? process.env);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (payload?.tool_name !== 'Bash' || !payload.agent_id) return null;
|
|
66
|
+
const command = payload.tool_input?.command || '';
|
|
67
|
+
const kind = testRunKind(command);
|
|
68
|
+
const verify = morphVerifyCall(command);
|
|
69
|
+
if (!kind && !verify) return null;
|
|
70
|
+
|
|
71
|
+
const env = deps.env ?? process.env;
|
|
72
|
+
const root = projectRoot(payload, env);
|
|
73
|
+
if (!stateExists(root)) return null;
|
|
74
|
+
const path = counterPath(root, payload.agent_id);
|
|
75
|
+
|
|
76
|
+
if (payload.hook_event_name === 'PreToolUse') {
|
|
77
|
+
if (REVIEW_AGENT_TYPES.has(payload.agent_type)) return blockResult(REASONS.review);
|
|
78
|
+
if (verify && !verify.filtered) return blockResult(REASONS.verify);
|
|
79
|
+
if (readRuns(path) >= TEST_RUN_BUDGET) return blockResult(REASONS.budget);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (payload.hook_event_name === 'PostToolUseFailure' && exitCodeOf(payload) === QUEUE_FULL_EXIT) return null;
|
|
84
|
+
if (payload.hook_event_name === 'PostToolUse' || payload.hook_event_name === 'PostToolUseFailure') recordRun(path, payload.agent_type);
|
|
85
|
+
} catch {}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await runStandalone(run);
|
|
Binary file
|
|
@@ -95,17 +95,76 @@ function kindOfTokens(tokens) {
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const segmentTokens = (segment) => skipWrappers(tokenize(segment.replace(/^[\s({]+/, '').replace(/[\s)}]+$/, '')));
|
|
99
|
+
|
|
98
100
|
// Judges only each segment's executable: `morph-spec heavy run -- dotnet test` already holds its
|
|
99
101
|
// own slot, and `echo "dotnet build"` runs nothing.
|
|
100
102
|
export function heavyKind(command) {
|
|
101
103
|
for (const segment of splitSegments(command)) {
|
|
102
|
-
const tokens =
|
|
104
|
+
const tokens = segmentTokens(segment);
|
|
103
105
|
const kind = kindOfTokens(tokens);
|
|
104
106
|
if (kind) return kind;
|
|
105
107
|
}
|
|
106
108
|
return null;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
export const REVIEW_AGENT_TYPES = new Set(['morph-verifier', 'morph-reviewer', 'morph-security']);
|
|
112
|
+
export const QUEUE_FULL_EXIT = 3;
|
|
113
|
+
|
|
114
|
+
const LIST_FLAGS = new Set(['--list', '--listTests', '--list-tests']);
|
|
115
|
+
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?))$/;
|
|
116
|
+
const NODE_RUNNERS = new Set(['vitest', 'jest']);
|
|
117
|
+
|
|
118
|
+
function morphArgs(tokens) {
|
|
119
|
+
if (executable(tokens[0] ?? '') === 'npx' && executable(tokens[1] ?? '') === 'morph-spec') return tokens.slice(2);
|
|
120
|
+
if (executable(tokens[0] ?? '') === 'morph-spec') return tokens.slice(1);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `morph-spec heavy run -- <cmd>` is peeled here, unlike in heavyKind: the slot is already held,
|
|
125
|
+
// but the run still spends a test round.
|
|
126
|
+
function* commandTokens(command) {
|
|
127
|
+
for (const segment of splitSegments(command)) {
|
|
128
|
+
const tokens = segmentTokens(segment);
|
|
129
|
+
const args = morphArgs(tokens);
|
|
130
|
+
if (args?.[0] === 'heavy' && args[1] === 'run') {
|
|
131
|
+
const rest = args.slice(2);
|
|
132
|
+
const dashes = rest.indexOf('--');
|
|
133
|
+
yield skipWrappers(dashes >= 0 ? rest.slice(dashes + 1) : rest);
|
|
134
|
+
} else {
|
|
135
|
+
yield tokens;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function testKindOfTokens(tokens) {
|
|
141
|
+
const [head, ...args] = tokens;
|
|
142
|
+
if (!head || args.some((a) => LIST_FLAGS.has(a) || INFO_FLAGS.has(a))) return null;
|
|
143
|
+
if (executable(head) === 'node') {
|
|
144
|
+
const runner = args.map((a) => executable(a).replace(/\.[cm]?js$/, '')).find((a) => NODE_RUNNERS.has(a));
|
|
145
|
+
return runner ?? null;
|
|
146
|
+
}
|
|
147
|
+
const kind = kindOfTokens(tokens);
|
|
148
|
+
return kind && TEST_LABEL.test(kind) ? kind : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function testRunKind(command) {
|
|
152
|
+
for (const tokens of commandTokens(command)) {
|
|
153
|
+
const kind = testKindOfTokens(tokens);
|
|
154
|
+
if (kind) return kind;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function morphVerifyCall(command) {
|
|
160
|
+
for (const tokens of commandTokens(command)) {
|
|
161
|
+
const args = morphArgs(tokens);
|
|
162
|
+
if (!args || firstArg(args) !== 'verify' || args.some((a) => INFO_FLAGS.has(a))) continue;
|
|
163
|
+
return { filtered: args.some((a) => a === '--filter' || a.startsWith('--filter=')) };
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
109
168
|
const E2E_CLASS = /\be2e\b|playwright|compose up/;
|
|
110
169
|
const UNIT_CLASS = /^(tests|(npx )?(vitest|jest)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?))$/;
|
|
111
170
|
|