@polymorphism-tech/morph-spec 10.2.0 → 10.5.3
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 +9 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +22 -2
- package/framework/evals/review-checklist.md +5 -5
- package/framework/hooks/claude-code/core/heavy-gate.js +21 -2
- package/framework/hooks/shared/background-spawn.js +20 -0
- package/framework/hooks/shared/graph-refresh.js +2 -1
- package/framework/hooks/shared/heavy-config.js +21 -0
- package/framework/hooks/shared/host-services.js +9 -6
- package/framework/hooks/shared/next-step.js +11 -7
- package/framework/rules/plan-grammar.md +4 -1
- package/framework/schemas/config.schema.json +39 -0
- package/framework/schemas/feature.schema.json +14 -0
- 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 +7 -4
- package/framework/skills/core/morph-apply/references/machine.md +3 -2
- package/framework/skills/core/morph-apply/references/review.md +5 -3
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-plan/references/critique.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +2 -1
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/project/heavy.js +13 -0
- package/src/commands/validation/verify.js +51 -5
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/heavy/gate.js +34 -2
- package/src/lib/heavy/reclaim.js +107 -0
- package/src/lib/heavy/watchdog.js +46 -0
- package/src/lib/next-step.js +1 -0
- package/src/lib/plan/checks.js +1 -1
- package/src/lib/procs/prune.js +23 -1
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/validators/verify-runner.js +34 -4
- package/src/lib/verify/affected.js +141 -0
- package/src/lib/verify/detached.js +156 -0
- package/src/utils/hooks-installer.js +8 -0
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
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.5.3
|
|
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.5.3 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -18,6 +18,7 @@ const doctorCommand = lazy('../src/commands/project/doctor.js', 'doctorCommand')
|
|
|
18
18
|
const statusCommand = lazy('../src/commands/project/status.js', 'statusCommand');
|
|
19
19
|
const heavyStatusCommand = lazy('../src/commands/project/heavy.js', 'heavyStatusCommand');
|
|
20
20
|
const heavyRunCommand = lazy('../src/commands/project/heavy.js', 'heavyRunCommand');
|
|
21
|
+
const heavyReclaimCommand = lazy('../src/commands/project/heavy.js', 'heavyReclaimCommand');
|
|
21
22
|
const worktreeSetupCommand = lazy('../src/commands/project/worktree.js', 'worktreeSetupCommand');
|
|
22
23
|
const worktreeProvisionCommand = lazy('../src/commands/project/worktree.js', 'worktreeProvisionCommand');
|
|
23
24
|
const worktreeLinkCommand = lazy('../src/commands/project/worktree.js', 'worktreeLinkCommand');
|
|
@@ -331,6 +332,8 @@ program
|
|
|
331
332
|
.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)')
|
|
332
333
|
.option('--no-persist', 'Do not persist the result to feature.json')
|
|
333
334
|
.option('--ci', 'Push morph/<feature> and take the proof from the project CI (creates .github/workflows/morph-verify.yml when no workflow runs verify). Same stamp, source "ci". No GitHub/gh/workflow = local verify with the reason; uncommitted code = exit 2; CI still running after 50 min = exit 3, no stamp')
|
|
335
|
+
.option('--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
|
+
.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')
|
|
334
337
|
.action((feature, options) =>
|
|
335
338
|
verifyCommand(feature, { ...options, noPersist: options.persist === false })
|
|
336
339
|
);
|
|
@@ -426,6 +429,12 @@ heavyCommand
|
|
|
426
429
|
.option('--json', 'Output as JSON')
|
|
427
430
|
.action((options) => heavyStatusCommand(options));
|
|
428
431
|
|
|
432
|
+
heavyCommand
|
|
433
|
+
.command('reclaim')
|
|
434
|
+
.description('Derruba stacks e2e ociosas de todos os projetos e containers de teste abandonados; dado de feature ativa fica')
|
|
435
|
+
.option('--json', 'Output as JSON')
|
|
436
|
+
.action((options) => heavyReclaimCommand(options));
|
|
437
|
+
|
|
429
438
|
heavyCommand
|
|
430
439
|
.command('run <command...>')
|
|
431
440
|
.description('Rodar um comando pesado dentro de um slot: morph-spec heavy run [--label x] -- dotnet build. Repassa o exit code; exit 3 = ocupado, não é vermelho')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/MORPH.md
CHANGED
|
@@ -10,7 +10,7 @@ Entre gates, você opera sozinho: faz o que `next` mandar (`implement`, `verify`
|
|
|
10
10
|
|
|
11
11
|
| Situação | O que você faz |
|
|
12
12
|
| --- | --- |
|
|
13
|
-
| Ambiguidade que o plano cobre | resolve pelo lado conservador e anota em `## Notas` do `report.md` |
|
|
13
|
+
| Ambiguidade que o plano cobre | resolve pelo lado que serve ao `Resultado buscado` (se um revert não desfaz, pelo conservador) e anota em `## Notas` do `report.md` |
|
|
14
14
|
| Ambiguidade bloqueante (requisito ausente ou contraditório) | uma pergunta via `AskUserQuestion` |
|
|
15
15
|
| `next` devolve `escalate` (3 verify vermelhos seguidos) | `AskUserQuestion`: aceitar, continuar ou parar. Nunca uma 4ª tentativa por conta própria |
|
|
16
16
|
| `verify` exit 3 | fila cheia, não vermelho: no máximo uma chamada nova |
|
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
|
|
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."
|
|
35
35
|
},
|
|
36
36
|
"reviewer": {
|
|
37
37
|
"title": "Gate 3 Reviewer",
|
package/framework/docs/CLI.md
CHANGED
|
@@ -319,6 +319,18 @@ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de con
|
|
|
319
319
|
|
|
320
320
|
**Por quê.** Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar.
|
|
321
321
|
|
|
322
|
+
### verify --affected
|
|
323
|
+
|
|
324
|
+
**`morph-spec verify <feature> --affected [--json] [--detach]`** — a rodada do dia a dia: build, validadores e só os testes que o diff contra o merge-base com a branch padrão alcança, sem e2e. Grava em `verifications.affected` (com `sha` e `failStreak`) e abre o dispatch do Gate 3, mas **nunca substitui o `verification`** que o gate lê: `gate-check review` segue exigindo uma rodada completa, que a skill faz uma vez, depois dos relatórios.
|
|
325
|
+
|
|
326
|
+
**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.
|
|
327
|
+
|
|
328
|
+
**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`.
|
|
329
|
+
|
|
330
|
+
### verify --detach
|
|
331
|
+
|
|
332
|
+
**`morph-spec verify <feature> [--json] --detach`** — roda o `verify` num processo fora da árvore da sessão: a chamada espera e devolve a mesma saída e o mesmo exit, mas uma sessão encerrada, ou o Claude Code matando comandos de fundo sob pressão de memória, não leva a rodada junto, e o stamp é gravado do mesmo jeito. Um processo lançador nasce e morre entre os dois, para que quem mata por árvore não alcance a rodada. O mesmo comando sobre a mesma árvore **retoma** (rodada viva: espera; terminada e não lida: devolve uma vez). Outra árvore com rodada viva da feature: espera ela acabar e abre a sua. Rodada que morreu sem resultado: exit 3, sem stamp, nunca vermelho. Estado em `verify-<id>.{json,out,err,exit}` no diretório do `heavy`. Quem contém a RAM é o piso antes de cada nó e o vigia durante a rodada (`### heavy`).
|
|
333
|
+
|
|
322
334
|
### verify --ci
|
|
323
335
|
|
|
324
336
|
**`morph-spec verify <feature> --ci [--json]`** — a prova de escopo de feature tirada do **CI do projeto** (GitHub Actions), não da máquina: faz `git push -u origin morph/<feature>`, espera o run do commit e transforma o `verify --json` que o run publicou (artefato `morph-verify`) no **mesmo stamp** de uma rodada local, com `source: "ci"` e `ciRun` (URL do run); `sha` é o `headSha` do run, então o `verificationStale` compara o código que o CI viu com o HEAD.
|
|
@@ -402,13 +414,21 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
|
|
|
402
414
|
|
|
403
415
|
**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` 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.
|
|
404
416
|
|
|
417
|
+
**Reclaim antes de esperar** (`verify`, `e2e up`, `heavy run`, o hook `heavy-gate` e `morph-spec heavy reclaim [--json]` à mão; `heavy.reclaimIdleMinutes`, 20, `0` desliga; `MORPH_HEAVY_RECLAIM=off` desliga por ambiente, e a suíte do framework roda assim): abaixo do piso, uma vez por rodada, derruba as stacks e2e **de todos os projetos registrados** que ninguém usa — a de feature arquivada, com volumes; a de feature ativa sem vaga na fila e sem evento em `.morph/logs/events.jsonl` há esse tempo, **mantendo os volumes** (o próximo `e2e up` só recompila) —, remove os containers de teste de `heavy.reclaimContainers` (`morph-pgtest`) quando nenhuma vaga pode estar testando, e encerra os build-servers ociosos do .NET. Nunca toca a stack da própria feature, nem stack sem dono, nem a de atividade desconhecida.
|
|
418
|
+
|
|
419
|
+
**Vigia durante o `verify --detach`** (`heavy.criticalFreeMemGb`, 1, `0` desliga): o piso julga o começo de um nó; o vigia, um processo próprio, lê a RAM a cada 10 s durante a rodada. Abaixo do valor, tenta o reclaim na 1ª leitura; seis leituras seguidas abaixo param **só aquela rodada**, com a árvore de processos dela: exit 3, sem stamp, nunca vermelho.
|
|
420
|
+
|
|
421
|
+
**Máquina responsiva** (`heavy.yieldToUser`, ligado): um quarto dos núcleos fica de fora (mínimo 2); a cada nó, a rodada sozinha usa o resto, e com outra na fila usa `resto / heavy.slots`, por `DOTNET_PROCESSOR_COUNT` (build e testes), `VITEST_MAX_FORKS`, `VITEST_MAX_THREADS` e `--maxWorkers`. Numa máquina de 16 núcleos: 12 sozinha, 6 acompanhada. Medido em duas suítes reais do Vitest: sem custo de tempo acompanhada, 4% sozinha, e metade da latência para abrir um processo. O comando solto, fora da fila, também respeita: `init` e `update` gravam a parte acompanhada em `env` de `.claude/settings.local.json`, sem sobrescrever valor que já esteja lá. Prioridade baixa fica de fora de propósito: numa máquina ocupada, o processo perde a disputa e falha pelo próprio limite de tempo.
|
|
422
|
+
|
|
423
|
+
**Fila que não recusa** (`heavy.detachedWaitMinutes`, 60): fora de um shell não há timeout de chamada, então o `verify --detach` espera a vez em vez de sair com exit 3 em `waitTimeoutSec`. `MORPH_HEAVY_WAIT_SEC` sobrepõe a espera.
|
|
424
|
+
|
|
405
425
|
**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.
|
|
406
426
|
|
|
407
427
|
**Estouro = exit 3 (`busy`), nunca vermelho:** `verify --json` sai `{ status: "busy", persisted: false, busy: { reason, holders, freeGb, waitedMs } }`, **sem stamp e sem evento `Verify`** (nada foi verificado, e fila não é gate verde nem vermelho); `e2e up` e `heavy run` também saem 3. Quem orquestra repete a chamada — nas skills, exit 3 não conta como tentativa e não dispara o loop de correção.
|
|
408
428
|
|
|
409
429
|
**Slot vago:** pid morto, ou idade acima de `heavy.maxHoldMinutes` (90, contra reuso de PID); Ctrl-C deixa o arquivo, que o pid morto cobre. Um filho herda o slot por `MORPH_HEAVY_LEASE` e nunca pede um segundo.
|
|
410
430
|
|
|
411
|
-
**Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `minFreeMemGbByKind`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado.
|
|
431
|
+
**Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `minFreeMemGbByKind`, `waitTimeoutSec`, `maxHoldMinutes`, `reclaimIdleMinutes`, `reclaimContainers`, `detachedWaitMinutes`, `criticalFreeMemGb`, `yieldToUser`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado.
|
|
412
432
|
|
|
413
433
|
Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`.
|
|
414
434
|
|
|
@@ -506,7 +526,7 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
506
526
|
|
|
507
527
|
### plan-check
|
|
508
528
|
|
|
509
|
-
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `2-plan/plan.html`, a visão só leitura que o gate do plano abre no navegador: cenários com prova/risco/degrau, padrões assumidos numerados e, com `## Estados` (opcional, `- origem → destino: gatilho` por linha), a máquina de estados desenhada. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`,
|
|
529
|
+
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `2-plan/plan.html`, a visão só leitura que o gate do plano abre no navegador: cenários com prova/risco/degrau, padrões assumidos numerados e, com `## Estados` (opcional, `- origem → destino: gatilho` por linha), a máquina de estados desenhada. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; `risco:` com `prova: leitura`; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`, ADR sem `**Alcance:** feature|projeto`, `## Fatias` que sobrou da 9.x (não é lida). Com o gate `plan` aprovado e o corpo mudado fora da `## Errata`, recusa (`frozen`, exit 1) — o `planHash` ignora a errata; mudança de escopo é `unapprove <f> plan`, `plan-check` e o gate de novo. Plano extraído antes das regras de `risco:` e de mídia não passa a falhar por elas. Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a. Com o plano válido, acrescenta ao `config.json` a stack que os arquivos mostram e instala em `.claude/rules/` a regra de stack que falta (uma linha por regra, em stderr; `rulesAdded` no `--json`).
|
|
510
530
|
|
|
511
531
|
### stack-set
|
|
512
532
|
|
|
@@ -33,7 +33,7 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
|
|
|
33
33
|
| `check` | verificador | cenário `Cn` NÃO PROVADO ou FALHA | o que falta provar, ou o repro que falha |
|
|
34
34
|
| `teste` | verificador | teste vermelho; teste que não morde a regra que diz proteger; Cn de risco sem teste | saída do teste filtrado, assert que passa pelo motivo errado |
|
|
35
35
|
| `regressao` | revisor | comportamento que funcionava e quebrou | consumidor afetado + o caminho que quebra |
|
|
36
|
-
| `escopo` | revisor | o diff faz o que nenhum cenário
|
|
36
|
+
| `escopo` | revisor | o diff faz o que nenhum cenário nem o `Resultado buscado` explica, deixa de fazer o que o contrato pede, ou o que você notou contradiz o `Resultado buscado` | trecho sem explicação, o item do contrato ausente, ou o fato e a frase do Resultado que ele desmente |
|
|
37
37
|
| `arquitetura` | revisor | padrão ou abstração nova contra o stack e os standards; item da lista fechada de `architecture-engineering-lens#review-blockers` que o DIFF introduziu (o que já existia é nota) | o trecho e o standard que ele contraria |
|
|
38
38
|
| `performance` | revisor | custo que o volume real torna visível | a consulta/lista e o volume que a derruba |
|
|
39
39
|
| `irreversivel` | revisor | o diff faz o que um revert não desfaz e nenhum `Cn` declara `degrau: migrar`/`irreversível`: apaga ou reescreve dado fora de migration, envia a terceiro, cobra, muda resposta de API ou webhook que alguém consome | o trecho e o efeito que fica depois do revert. Vai direto ao usuário, sem rodada de correção |
|
|
@@ -64,7 +64,7 @@ linha de cada Cn.
|
|
|
64
64
|
|
|
65
65
|
| `prova:` | Execução |
|
|
66
66
|
|---|---|
|
|
67
|
-
| `e2e` | Navegue o fluxo com o Playwright MCP na URL do dispatch, como o usuário faria; confira o resultado na tela, no console e no network. Rode `morph-spec e2e reset {feature}` **antes de cada fluxo**: estado de um fluxo nunca prova o seguinte. |
|
|
67
|
+
| `e2e` | Navegue o fluxo com o Playwright MCP na URL do dispatch, como o usuário faria, disparando a ação até o efeito (controle presente não prova nada); confira o resultado na tela, no console e no network. Rode `morph-spec e2e reset {feature}` **antes de cada fluxo**: estado de um fluxo nunca prova o seguinte. |
|
|
68
68
|
| `teste` | Ache o teste que cobre o Cn e rode **só ele**, pelo filtro do próprio runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`, `npx playwright test <arquivo>`). `verify --filter` escolhe um nó inteiro da suíte: só serve quando o nó é esse teste. Verde não basta: leia o teste — ele morde? Nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, ou o assert passa pelo motivo errado → bloqueador `teste`. Sem teste para um Cn com `risco:` → `teste`. |
|
|
69
69
|
| `eval` | Rode o nó de evals filtrado (`--filter <nó-evals> --skip-e2e --no-persist`) e compare a saída com o cenário. |
|
|
70
70
|
| `leitura` | `arquivo:linha` do código que entrega o cenário e a **fiação**: caminho da entrada real ao efeito (a camada pura não prova que o handler a chama). Sem teste, e pedir teste não é bloqueador. |
|
|
@@ -74,8 +74,8 @@ Verificações mecânicas valem para todo Cn de backend (.NET minimal API / Next
|
|
|
74
74
|
lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como
|
|
75
75
|
constantes distintas.
|
|
76
76
|
|
|
77
|
-
Você não roda a suíte inteira nem `verify` sem filtro (o hook `test-budget` nega):
|
|
78
|
-
|
|
77
|
+
Você não roda a suíte inteira nem `verify` sem filtro (o hook `test-budget` nega): antes do dispatch
|
|
78
|
+
rodaram os testes que o diff alcança, e a suíte completa roda depois dos relatórios. Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — achado de
|
|
79
79
|
infra, não defeito seu a provar; não suba nem derrube o stack.
|
|
80
80
|
|
|
81
81
|
**Feature que gera mídia** (o `plan.md` cita um standard de mídia): grave `## Mídia gerada`, uma
|
|
@@ -101,7 +101,7 @@ havendo item decidido, o gate pausa.
|
|
|
101
101
|
Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, e o
|
|
102
102
|
`2-plan/plan.md` (a spec). Nunca a árvore de trabalho: o commit é o que foi provado. Default SHIP.
|
|
103
103
|
|
|
104
|
-
1. **Escopo**: o diff contra os cenários e o `## Contrato de implementação` do plano.
|
|
104
|
+
1. **Escopo**: o diff contra os cenários, o `Resultado buscado` e o `## Contrato de implementação` do plano.
|
|
105
105
|
Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
|
|
106
106
|
risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
|
|
107
107
|
camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê nas `## Notas` do
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
// The lease has no live pid (the hook exits at once), so it lapses by TTL; a call that fires no Post hook holds it until then.
|
|
4
4
|
// The wait stays under the 600 s hook timeout because a timed-out PreToolUse hook lets the call run.
|
|
5
5
|
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { freemem } from 'node:os';
|
|
6
8
|
import { stateExists } from '../../shared/state-reader.js';
|
|
7
9
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
8
10
|
import { heavyClass, heavyKind, leaseTtlMs } from '../../shared/heavy-command.js';
|
|
9
11
|
|
|
10
12
|
const HOOK_BUDGET_SEC = 540;
|
|
13
|
+
const RECLAIM_TIMEOUT_MS = 45_000;
|
|
14
|
+
|
|
15
|
+
const defaultFreeGb = () => freemem() / 1024 ** 3;
|
|
16
|
+
|
|
17
|
+
// Through the CLI: an installed hook cannot reach the package's src/.
|
|
18
|
+
function runReclaim(cwd, env) {
|
|
19
|
+
if (env.MORPH_HEAVY_RECLAIM === 'off') return;
|
|
20
|
+
spawnSync('morph-spec heavy reclaim --json', {
|
|
21
|
+
cwd, env, shell: true, windowsHide: true, stdio: 'ignore', timeout: RECLAIM_TIMEOUT_MS,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export async function run(payload, deps = {}) {
|
|
13
26
|
try {
|
|
@@ -27,6 +40,12 @@ export async function run(payload, deps = {}) {
|
|
|
27
40
|
if (!config.enabled || !config.gateBash) return null;
|
|
28
41
|
|
|
29
42
|
const memClass = heavyClass(kind);
|
|
43
|
+
const floor = minFreeMemFor(config, memClass);
|
|
44
|
+
let spentSec = 0;
|
|
45
|
+
if (config.reclaimIdleMinutes && (deps.freeGb ?? defaultFreeGb)() < floor) {
|
|
46
|
+
(deps.reclaim ?? runReclaim)(dir, env);
|
|
47
|
+
spentSec = RECLAIM_TIMEOUT_MS / 1000;
|
|
48
|
+
}
|
|
30
49
|
const result = (deps.acquire ?? acquireHeavy)({
|
|
31
50
|
dir: heavyDir(env),
|
|
32
51
|
slots: config.slots,
|
|
@@ -35,8 +54,8 @@ export async function run(payload, deps = {}) {
|
|
|
35
54
|
node: null,
|
|
36
55
|
cwd: dir,
|
|
37
56
|
kind: memClass,
|
|
38
|
-
minFreeMemGb:
|
|
39
|
-
waitTimeoutMs: Math.min(config.waitTimeoutSec, HOOK_BUDGET_SEC) * 1000,
|
|
57
|
+
minFreeMemGb: floor,
|
|
58
|
+
waitTimeoutMs: Math.max(0, Math.min(config.waitTimeoutSec, HOOK_BUDGET_SEC) - spentSec) * 1000,
|
|
40
59
|
maxHoldMs: config.maxHoldMinutes * 60_000,
|
|
41
60
|
lease: { id: payload.tool_use_id, ttlMs: leaseTtlMs(input, env) },
|
|
42
61
|
env,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const HOST = fileURLToPath(import.meta.url);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Arguments for a detached node process. On Windows `detached` means no console at all and
|
|
8
|
+
* `windowsHide` is ignored, so every console child opens a visible window; a child that is not
|
|
9
|
+
* detached has a hidden console but dies with its node parent (libuv job). The host holds both
|
|
10
|
+
* ends: detached itself, it stays alive for a child whose hidden console all descendants inherit.
|
|
11
|
+
*/
|
|
12
|
+
export function backgroundArgs(args, platform = process.platform) {
|
|
13
|
+
return platform === 'win32' ? [HOST, ...args] : args;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (process.argv[1] === HOST) {
|
|
17
|
+
const child = spawn(process.execPath, process.argv.slice(2), { stdio: 'inherit', windowsHide: true });
|
|
18
|
+
child.on('error', () => process.exit(1));
|
|
19
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
20
|
+
}
|
|
@@ -12,6 +12,7 @@ import { homedir } from 'os';
|
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { readPrimaryHead, resolveGitCommonDir, workTreeTop } from './git-paths.js';
|
|
14
14
|
import { findFirstOnPath, quoteForCmd } from './morph-cli.js';
|
|
15
|
+
import { backgroundArgs } from './background-spawn.js';
|
|
15
16
|
import { ensureCommonExclude } from './git-exclude.js';
|
|
16
17
|
import { pidAlive } from './process-alive.js';
|
|
17
18
|
import {
|
|
@@ -274,7 +275,7 @@ export function kickGraphRefresh(dir, { spawnFn = spawn, env = process.env } = {
|
|
|
274
275
|
return { kicked: false, reason: 'graphify-unavailable' };
|
|
275
276
|
}
|
|
276
277
|
try {
|
|
277
|
-
const child = spawnFn(process.execPath, [WORKER, '--run', root], {
|
|
278
|
+
const child = spawnFn(process.execPath, backgroundArgs([WORKER, '--run', root]), {
|
|
278
279
|
cwd: resolveGitCommonDir(root), env, detached: true, stdio: 'ignore', windowsHide: true,
|
|
279
280
|
});
|
|
280
281
|
child.on?.('error', () => {});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { availableParallelism } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
|
|
4
5
|
const CONFIG_FILE = join('.morph', 'config', 'config.json');
|
|
@@ -10,6 +11,11 @@ export const HEAVY_DEFAULTS = Object.freeze({
|
|
|
10
11
|
minFreeMemGbByKind: Object.freeze({ unit: 1.5, build: 4, e2e: 4 }),
|
|
11
12
|
waitTimeoutSec: 600,
|
|
12
13
|
maxHoldMinutes: 90,
|
|
14
|
+
reclaimIdleMinutes: 20,
|
|
15
|
+
reclaimContainers: Object.freeze(['morph-pgtest']),
|
|
16
|
+
detachedWaitMinutes: 60,
|
|
17
|
+
criticalFreeMemGb: 1,
|
|
18
|
+
yieldToUser: true,
|
|
13
19
|
shutdownBuildServer: true,
|
|
14
20
|
disableNodeReuse: false,
|
|
15
21
|
gateBash: true,
|
|
@@ -17,6 +23,14 @@ export const HEAVY_DEFAULTS = Object.freeze({
|
|
|
17
23
|
|
|
18
24
|
export const HEAVY_KINDS = Object.freeze(['unit', 'build', 'e2e']);
|
|
19
25
|
|
|
26
|
+
export const CPU_SHARE_ENVS = Object.freeze(['DOTNET_PROCESSOR_COUNT', 'VITEST_MAX_FORKS', 'VITEST_MAX_THREADS']);
|
|
27
|
+
|
|
28
|
+
/** A quarter of the machine stays with the person; what is left goes to one run, or is split by the slots. */
|
|
29
|
+
export function cpuShareOf(slots, { cores = availableParallelism(), alone = false } = {}) {
|
|
30
|
+
const usable = cores - Math.max(2, Math.floor(cores / 4));
|
|
31
|
+
return Math.max(2, alone ? usable : Math.floor(usable / slots));
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
const isBool = (v) => typeof v === 'boolean';
|
|
21
35
|
const isGb = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
22
36
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -27,6 +41,11 @@ const VALID = {
|
|
|
27
41
|
minFreeMemGbByKind: isPlainObject,
|
|
28
42
|
waitTimeoutSec: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
29
43
|
maxHoldMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v > 0,
|
|
44
|
+
reclaimIdleMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
45
|
+
yieldToUser: isBool,
|
|
46
|
+
reclaimContainers: (v) => Array.isArray(v) && v.every((p) => typeof p === 'string' && /^[\w.-]+$/.test(p)),
|
|
47
|
+
detachedWaitMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
48
|
+
criticalFreeMemGb: isGb,
|
|
30
49
|
shutdownBuildServer: isBool,
|
|
31
50
|
disableNodeReuse: isBool,
|
|
32
51
|
gateBash: isBool,
|
|
@@ -66,6 +85,8 @@ export function resolveHeavyConfig(config, env = process.env) {
|
|
|
66
85
|
if (/^(off|0|false)$/i.test(env.MORPH_HEAVY ?? '')) out.enabled = false;
|
|
67
86
|
const envSlots = Number(env.MORPH_HEAVY_SLOTS);
|
|
68
87
|
if (env.MORPH_HEAVY_SLOTS !== undefined && VALID.slots(envSlots)) out.slots = envSlots;
|
|
88
|
+
const envWait = Number(env.MORPH_HEAVY_WAIT_SEC);
|
|
89
|
+
if (env.MORPH_HEAVY_WAIT_SEC && VALID.waitTimeoutSec(envWait)) out.waitTimeoutSec = envWait;
|
|
69
90
|
|
|
70
91
|
return { ...out, warnings };
|
|
71
92
|
}
|
|
@@ -134,7 +134,7 @@ function probeHealth(url, timeoutMs = 1_500) {
|
|
|
134
134
|
|
|
135
135
|
// Only the recorded identity may be killed: a recycled PID belongs to someone else.
|
|
136
136
|
function killRecord(record, readCreatedAt) {
|
|
137
|
-
if (!
|
|
137
|
+
if (!processRunning(record.pid)) {
|
|
138
138
|
return { killed: false, warning: `hostService "${record.name}": PID ${record.pid} was no longer running; record dropped` };
|
|
139
139
|
}
|
|
140
140
|
const current = readCreatedAt(record.pid);
|
|
@@ -157,11 +157,14 @@ function waitUntil(predicate) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// The process that spawned the shell blocks in a sync loop and never reaps it: a zombie is not alive.
|
|
160
|
-
function
|
|
160
|
+
export function processRunning(pid) {
|
|
161
161
|
if (!pidAlive(pid)) return false;
|
|
162
162
|
if (process.platform === 'win32') return true;
|
|
163
163
|
const r = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8' });
|
|
164
|
-
|
|
164
|
+
if (r.error) return true;
|
|
165
|
+
// ps exits non-zero when the process vanished between pidAlive and here.
|
|
166
|
+
const stat = String(r.stdout).trim();
|
|
167
|
+
return stat !== '' && !stat.startsWith('Z');
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
function groupAlive(pgid) {
|
|
@@ -325,13 +328,13 @@ function launch(svc, ctx, deadline, exec) {
|
|
|
325
328
|
|
|
326
329
|
if (!svc.health) {
|
|
327
330
|
sleep(Math.min(NO_HEALTH_GRACE_MS, Math.max(remaining(), 0)));
|
|
328
|
-
if (!
|
|
331
|
+
if (!processRunning(child.pid)) return { ...fail('exited right after start', readLogTail(log)), record };
|
|
329
332
|
return { ok: true, record };
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
while (true) {
|
|
333
336
|
if (probeHealth(svc.health)) return { ok: true, record };
|
|
334
|
-
if (!
|
|
337
|
+
if (!processRunning(child.pid)) return { ...fail('exited before its health check went green', readLogTail(log)), record };
|
|
335
338
|
if (remaining() <= 0) {
|
|
336
339
|
return { ...fail(`health ${svc.health} not green within ${svc.timeoutMs} ms`, readLogTail(log)), record };
|
|
337
340
|
}
|
|
@@ -340,7 +343,7 @@ function launch(svc, ctx, deadline, exec) {
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
function isReusable(record, svc, ctx) {
|
|
343
|
-
if (!record || !
|
|
346
|
+
if (!record || !processRunning(record.pid)) return false;
|
|
344
347
|
if (ctx.readCreatedAt(record.pid) !== record.createdAt) return false;
|
|
345
348
|
return svc.health ? probeHealth(svc.health) : true;
|
|
346
349
|
}
|
|
@@ -30,10 +30,10 @@ const HOW = {
|
|
|
30
30
|
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando; resposta que muda escopo vai por `unapprove`.',
|
|
31
31
|
],
|
|
32
32
|
verify: (f) => [
|
|
33
|
-
`\`morph-spec verify ${f} --json
|
|
33
|
+
`\`morph-spec verify ${f} --affected --json --detach\`, em run_in_background: só os testes que o diff alcança; repetir o comando retoma a rodada.`,
|
|
34
34
|
],
|
|
35
35
|
fix: (f) => [
|
|
36
|
-
`Leia o \`feedback\` do último verify, corrija a causa e rode
|
|
36
|
+
`Leia o \`feedback\` do último verify de ${f}, corrija a causa e rode o comando acima de novo, com \`--json\`.`,
|
|
37
37
|
],
|
|
38
38
|
escalate: () => [
|
|
39
39
|
'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
|
|
@@ -87,19 +87,22 @@ function decide(s) {
|
|
|
87
87
|
if (pending.length) {
|
|
88
88
|
return step('implement', skill('morph-apply'), 'implemente nesta sessão e marque cada Cn feito no report.md', { checks: pending });
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const fullFresh = s.verification?.status && s.verificationStale !== true;
|
|
91
|
+
const affectedFresh = s.affected?.status && s.affectedStale !== true;
|
|
92
|
+
const v = fullFresh ? s.verification : affectedFresh ? s.affected : null;
|
|
93
|
+
if (!v) {
|
|
94
|
+
const ran = s.verification?.status || s.affected?.status;
|
|
95
|
+
return step('verify', `morph-spec verify ${f} --affected`, ran ? 'a árvore andou desde o último verify' : 'todo Cn feito, sem verify');
|
|
93
96
|
}
|
|
94
97
|
if (v.status === 'fail') {
|
|
95
|
-
const streak = s.failStreak ?? 0;
|
|
98
|
+
const streak = (fullFresh ? s.failStreak : s.affected.failStreak) ?? 0;
|
|
96
99
|
if (streak > 0 && streak % FIX_ATTEMPTS === 0) {
|
|
97
100
|
return step('escalate', 'AskUserQuestion (aceitar/continuar/parar)',
|
|
98
101
|
`${streak} verifies vermelhos seguidos, nunca uma tentativa a mais por conta própria. `
|
|
99
102
|
+ `aceitar → ${skill('morph-review')} (o verify vermelho pausa o gate: o humano assina lá); `
|
|
100
103
|
+ 'continuar → corrija e rode o verify de novo; parar → encerre aqui. Segue `escalate` até a árvore andar');
|
|
101
104
|
}
|
|
102
|
-
return step('fix', `morph-spec verify ${f}`,
|
|
105
|
+
return step('fix', `morph-spec verify ${f}${fullFresh ? '' : ' --affected'}`,
|
|
103
106
|
`corrija pelo feedback do verify e rode de novo (${FIX_ATTEMPTS - (streak % FIX_ATTEMPTS)} tentativa(s) antes de perguntar)`);
|
|
104
107
|
}
|
|
105
108
|
return step('review', skill('morph-review'), 'verify verde: Gate 3');
|
|
@@ -142,5 +145,6 @@ export function collectNextSignals(featureName, cwd) {
|
|
|
142
145
|
gates: feature.approvals || feature.approvalGates || {},
|
|
143
146
|
verification: feature.verification ?? null,
|
|
144
147
|
failStreak: feature.verifications?.feature?.failStreak ?? 0,
|
|
148
|
+
affected: feature.verifications?.affected ?? null,
|
|
145
149
|
};
|
|
146
150
|
}
|
|
@@ -13,6 +13,9 @@ paths:
|
|
|
13
13
|
|
|
14
14
|
- **Teste é por risco, não por código novo.** `prova: teste` exige `risco:` (`regra` | `dinheiro` | `contrato` | `concorrência` | `dado` | `regressão`): ramo de regra de negócio, valor, contrato externo, corrida, integridade de dado ou o bug real. Sem risco (CRUD, DTO, mapping, wiring, constante, texto, layout) a prova é `leitura` — o verificador fecha por `arquivo:linha` — ou `e2e` quando o fluxo é o que importa.
|
|
15
15
|
- **Degrau de retrabalho** (opcional, junto de `prova:`): `degrau:` `refazer` | `desfazer` | `migrar` | `irreversível`; sem a linha, `refazer`. `migrar` é toda migration, aditiva ou não (drop, rename, backfill, coluna nova); `irreversível` apaga dado que alguém lê ou quebra contrato externo. Os dois pausam o gate do plano para o usuário, e o Gate 3 pausa quando a feature toca `migrations/` sem nenhum dos dois, ou um arquivo de contrato (`openapi.yaml`, `swagger.json`, `.proto`) sem `irreversível`; o revisor do Gate 3 caça o resto (webhook, dado apagado fora de migration, envio a terceiro).
|
|
16
|
+
- **Quem tem `risco:` prova-se executando.** `risco:` com `prova: leitura` é erro.
|
|
17
|
+
- **`e2e` dispara a ação do dono até o efeito** (salvar, ativar, enviar): conferir que o controle aparece não prova. Dois estados (rascunho e ativo) são dois passos no mesmo `Cn`.
|
|
18
|
+
- **Número que soma ou confere com outro** roda sobre dado com a forma do de produção; fixture feita à mão só tem as formas que você já imaginou.
|
|
16
19
|
- **Observável**: que asserção, ou que passo no navegador, prova? Sem resposta, reescreva ou descarte.
|
|
17
20
|
- **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório. Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`, que o parser não lê).
|
|
18
21
|
- **Saída de LLM** prova-se por `eval` de runtime, nunca pelo texto do prompt: evidência em `3-implement/evidencias/{Cn}.md`; com nó `runner: "evals"` (standard `ai-agents-evals-with-cache`) o cenário mora no dataset.
|
|
@@ -50,4 +53,4 @@ Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatur
|
|
|
50
53
|
|
|
51
54
|
## O que o plan-check recusa
|
|
52
55
|
|
|
53
|
-
Veredito de reuso fora do enum, id `C`/`ADR` duplicado, `## Errata` fora do fim, `## Cenários` vazio, `teste` sem `risco:`, bug sem cenário de regressão, `## Padrões assumidos` ausente ou fora de `1..N`, `## Estados` com linha que não é `origem → destino: gatilho`. Avisa: padrão que toca credencial, permissão, pagamento ou apagar dado, chave desconhecida (inclusive o `## Fatias` de planos antigos), ADR sem **Alcance**.
|
|
56
|
+
Veredito de reuso fora do enum, id `C`/`ADR` duplicado, `## Errata` fora do fim, `## Cenários` vazio, `teste` sem `risco:`, `risco:` com `leitura`, bug sem cenário de regressão, `## Padrões assumidos` ausente ou fora de `1..N`, `## Estados` com linha que não é `origem → destino: gatilho`. Avisa: padrão que toca credencial, permissão, pagamento ou apagar dado, chave desconhecida (inclusive o `## Fatias` de planos antigos), ADR sem **Alcance**.
|
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
"enum": ["dotnet", "vitest", "jest", "node-test", "playwright", "none", "evals"],
|
|
55
55
|
"description": "Chooses the test-count extractor and whether a prebuild applies. Inferred from `command` when omitted; `none` means the count is UNKNOWN, never zero. `evals` is the ONLY value that is never inferred - it is a declaration that this suite reads a committed response cache, and it REQUIRES the `evals` block below."
|
|
56
56
|
},
|
|
57
|
+
"affected": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["flag"],
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "Rarely needed: `verify --affected` reads an inline filter, a `$Filter` parameter of the script or a script that forwards its arguments by itself. Declare this only for a command none of those reach, which otherwise runs whole. Wins over the detection.",
|
|
62
|
+
"properties": {
|
|
63
|
+
"flag": { "type": "string", "minLength": 1, "description": "The argument of the command that takes a test filter, e.g. -Filter for a wrapper script." },
|
|
64
|
+
"baseFilter": { "type": "string", "description": "Filter the command applies by default, kept alongside the selection, e.g. Category!=Eval." }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
57
67
|
"expectedCount": {
|
|
58
68
|
"type": "integer",
|
|
59
69
|
"minimum": 0,
|
|
@@ -268,6 +278,35 @@
|
|
|
268
278
|
"e2e": { "type": "number", "minimum": 0 }
|
|
269
279
|
}
|
|
270
280
|
},
|
|
281
|
+
"reclaimIdleMinutes": {
|
|
282
|
+
"type": "number",
|
|
283
|
+
"minimum": 0,
|
|
284
|
+
"default": 20,
|
|
285
|
+
"description": "Below the RAM floor, before waiting, `verify`, `e2e up` and `heavy run` bring down the e2e stacks of this repo nobody is using: an archived feature's (with volumes) and an active feature's idle for this many minutes, with no heavy slot (volumes kept). 0 turns it off."
|
|
286
|
+
},
|
|
287
|
+
"reclaimContainers": {
|
|
288
|
+
"type": "array",
|
|
289
|
+
"items": { "type": "string", "pattern": "^[\\w.-]+$" },
|
|
290
|
+
"default": ["morph-pgtest"],
|
|
291
|
+
"description": "Name prefixes of disposable test containers that reclaim removes when no heavy slot may be testing. A killed test run leaves these behind."
|
|
292
|
+
},
|
|
293
|
+
"detachedWaitMinutes": {
|
|
294
|
+
"type": "number",
|
|
295
|
+
"minimum": 0,
|
|
296
|
+
"default": 60,
|
|
297
|
+
"description": "How long a `verify --detach` waits in the queue. It runs out of any shell's timeout, so with several sessions open it waits for its turn instead of exiting 3."
|
|
298
|
+
},
|
|
299
|
+
"yieldToUser": {
|
|
300
|
+
"type": "boolean",
|
|
301
|
+
"default": true,
|
|
302
|
+
"description": "Heavy runs leave room for what the person is using: a quarter of the cores stays out, a run alone takes the rest and a run with company takes rest / slots, through DOTNET_PROCESSOR_COUNT, VITEST_MAX_FORKS, VITEST_MAX_THREADS and --maxWorkers. `init` and `update` write the shared value to `env` in .claude/settings.local.json for what runs outside the queue. Priority is left alone on purpose: below normal, a process starves on a busy machine and fails by its own timeouts."
|
|
303
|
+
},
|
|
304
|
+
"criticalFreeMemGb": {
|
|
305
|
+
"type": "number",
|
|
306
|
+
"minimum": 0,
|
|
307
|
+
"default": 1,
|
|
308
|
+
"description": "While a `verify --detach` runs, free RAM below this for a minute reclaims idle stacks and then stops that run with exit 3, no stamp. The floors judge the start of a node; this judges the middle. 0 turns it off."
|
|
309
|
+
},
|
|
271
310
|
"waitTimeoutSec": {
|
|
272
311
|
"type": "number",
|
|
273
312
|
"minimum": 0,
|
|
@@ -105,6 +105,20 @@
|
|
|
105
105
|
"updatedAt": { "type": "string", "format": "date-time" }
|
|
106
106
|
},
|
|
107
107
|
"additionalProperties": false
|
|
108
|
+
},
|
|
109
|
+
"affected": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"description": "Last `verify --affected`. Opens the Gate 3 dispatch and never stands in for the complete run the review gate reads.",
|
|
112
|
+
"required": ["status", "nodes", "attempts", "updatedAt"],
|
|
113
|
+
"properties": {
|
|
114
|
+
"status": { "type": "string", "enum": ["pass", "fail"] },
|
|
115
|
+
"nodes": { "type": "object" },
|
|
116
|
+
"attempts": { "type": "integer", "minimum": 1 },
|
|
117
|
+
"failStreak": { "type": "integer", "minimum": 0 },
|
|
118
|
+
"updatedAt": { "type": "string", "format": "date-time" },
|
|
119
|
+
"sha": { "type": ["string", "null"] }
|
|
120
|
+
},
|
|
121
|
+
"additionalProperties": false
|
|
108
122
|
}
|
|
109
123
|
},
|
|
110
124
|
"additionalProperties": false
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.3"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.3"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Implementa o plano aprovado de uma feature morph-spec inteiro nesta
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado ("/morph-apply {feature}", "implementar feature"), em "/morph-review {feature}" (entra no §5), em "/morph-hotfix {feature}" ou incidente de produção (references/hotfix.md), ou quando `morph-spec next` mandar implement, verify, fix, review ou finish
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -24,7 +24,8 @@ O plano inteiro, nesta sessão, em sequência. Sem sub-agent para implementar: o
|
|
|
24
24
|
- As regras de cada domínio (.NET, EF, MAF, Next.js, infra) chegam sozinhas: a rule do stack carrega quando você toca o arquivo. O texto inteiro de um standard: `morph-spec standards --find` → `--show <id>#<âncora>`.
|
|
25
25
|
- Antes de criar um tipo, confira a tabela `## Reuso` e `morph-spec graph explain <nome>` (sem grafo, `grep -rn`).
|
|
26
26
|
- Rode build e teste quando quiser; teste pesado fora do `verify` por `morph-spec heavy run -- <cmd>`. Exit 3 = fila cheia: uma chamada nova no máximo (`references/machine.md`).
|
|
27
|
-
-
|
|
27
|
+
- **Execute antes de marcar `feito`.** `teste`: rode e veja falhar sem a mudança. Soma ou conferência de número: ao menos uma vez sobre dado com a forma do de produção. `e2e` fica para a passada do §3.
|
|
28
|
+
- Ao fechar um `Cn`, atualize a linha dele no `report.md` (`feito` + a evidência: o que você executou e viu, teste, `arquivo:linha`); commite por lote coerente (`wip(<f>): C2-C3 …`), não por `Cn`. Decisão ou fato novo → uma linha em `## Notas`.
|
|
28
29
|
- Mídia gerada por IA: `references/media.md`. Erro difícil: `references/debugging.md`.
|
|
29
30
|
|
|
30
31
|
`Cn` que depende de uma decisão que não é sua: status `bloqueado` e a pergunta em `## Notas`; o `next` devolve `decide` → `AskUserQuestion`.
|
|
@@ -40,9 +41,11 @@ O plano inteiro, nesta sessão, em sequência. Sem sub-agent para implementar: o
|
|
|
40
41
|
|
|
41
42
|
## 3. Verify, sem filtro
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
**Passada de bancada:** com `Cn` `prova: e2e`, antes do verify e uma vez só (cada edição recompila a stack): `morph-spec e2e up {feature}` e dispare cada fluxo pela ferramenta da sessão (Playwright MCP, ou o spec do fluxo) até o efeito, não até o controle aparecer. É o que tira do Gate 3 o que uma execução acha. Não subiu ou exit 3: uma linha em `## Notas` e siga para o verify, sem parar a sessão.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Todo `Cn` `feito`: commite tudo e rode `morph-spec verify {feature} --affected --json --detach`: build, validadores e só os testes que o diff alcança. Migration, contrato, arquivo que alcança o projeto inteiro ou código compartilhado sobem sozinhos para a rodada completa. A completa, sem filtro, roda uma vez, depois dos relatórios do Gate 3 (`references/review.md` §6): é a prova que o Gate 3 e o CI leem, e `--affected` nunca a substitui. Sempre em `run_in_background`, esperando a notificação **no mesmo turno**, sem ler o log em laço. O `--detach` roda fora da sessão: encerrada a espera sem resultado, rode o mesmo comando, que retoma a rodada em vez de repetir. Nó `node-test`/`evals` sem `expectedCount`, ou teste apagado de propósito: `--record-baseline` na rodada completa, que é sem filtro.
|
|
47
|
+
|
|
48
|
+
Vermelho → corrija com `verify.feedback` e rode a mesma rodada de novo. Na completa, com código novo, as suítes vermelhas rodam primeiro e o resto só se passarem; sem mudança de código, só o que ficou vermelho roda. Mudou algo fora da árvore (`.env`, banco): `--fresh`. `next` conta: `fix` enquanto há tentativa, `escalate` na 3ª vermelha seguida → `AskUserQuestion` ("Verify de {feature} vermelho após 3 tentativas — aceitar com ressalva, continuar tentando, ou parar?").
|
|
46
49
|
|
|
47
50
|
## 4. Ramo por tipo
|
|
48
51
|
|