@polymorphism-tech/morph-spec 10.2.0 → 10.5.2
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/heavy-config.js +12 -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 +24 -1
- 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 +154 -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.2
|
|
6
|
+
**Version:** 10.5.2
|
|
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.2
|
|
379
|
+
*morph-spec v10.5.2 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): cada rodada usa só a sua parte dos núcleos (`núcleos / heavy.slots`, mínimo 2) por `DOTNET_PROCESSOR_COUNT` (build e testes) e `--maxWorkers` no Vitest. 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. O preço é rodada um pouco mais lenta. `DOTNET_PROCESSOR_COUNT` já definido no ambiente vence. O hook `heavy-gate` toma vaga, mas não muda o comando do Bash: lá isso não se aplica.
|
|
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,
|
|
@@ -10,6 +10,11 @@ export const HEAVY_DEFAULTS = Object.freeze({
|
|
|
10
10
|
minFreeMemGbByKind: Object.freeze({ unit: 1.5, build: 4, e2e: 4 }),
|
|
11
11
|
waitTimeoutSec: 600,
|
|
12
12
|
maxHoldMinutes: 90,
|
|
13
|
+
reclaimIdleMinutes: 20,
|
|
14
|
+
reclaimContainers: Object.freeze(['morph-pgtest']),
|
|
15
|
+
detachedWaitMinutes: 60,
|
|
16
|
+
criticalFreeMemGb: 1,
|
|
17
|
+
yieldToUser: true,
|
|
13
18
|
shutdownBuildServer: true,
|
|
14
19
|
disableNodeReuse: false,
|
|
15
20
|
gateBash: true,
|
|
@@ -27,6 +32,11 @@ const VALID = {
|
|
|
27
32
|
minFreeMemGbByKind: isPlainObject,
|
|
28
33
|
waitTimeoutSec: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
29
34
|
maxHoldMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v > 0,
|
|
35
|
+
reclaimIdleMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
36
|
+
yieldToUser: isBool,
|
|
37
|
+
reclaimContainers: (v) => Array.isArray(v) && v.every((p) => typeof p === 'string' && /^[\w.-]+$/.test(p)),
|
|
38
|
+
detachedWaitMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
39
|
+
criticalFreeMemGb: isGb,
|
|
30
40
|
shutdownBuildServer: isBool,
|
|
31
41
|
disableNodeReuse: isBool,
|
|
32
42
|
gateBash: isBool,
|
|
@@ -66,6 +76,8 @@ export function resolveHeavyConfig(config, env = process.env) {
|
|
|
66
76
|
if (/^(off|0|false)$/i.test(env.MORPH_HEAVY ?? '')) out.enabled = false;
|
|
67
77
|
const envSlots = Number(env.MORPH_HEAVY_SLOTS);
|
|
68
78
|
if (env.MORPH_HEAVY_SLOTS !== undefined && VALID.slots(envSlots)) out.slots = envSlots;
|
|
79
|
+
const envWait = Number(env.MORPH_HEAVY_WAIT_SEC);
|
|
80
|
+
if (env.MORPH_HEAVY_WAIT_SEC && VALID.waitTimeoutSec(envWait)) out.waitTimeoutSec = envWait;
|
|
69
81
|
|
|
70
82
|
return { ...out, warnings };
|
|
71
83
|
}
|
|
@@ -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: each run keeps to its share of the cores (cores / slots) through DOTNET_PROCESSOR_COUNT and vitest --maxWorkers. 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.2
|
|
7
|
+
cliVersion: "10.5.2"
|
|
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.2
|
|
7
|
+
cliVersion: "10.5.2"
|
|
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.2
|
|
7
|
+
cliVersion: "10.5.2"
|
|
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
|
|
|
@@ -14,8 +14,9 @@ Para manter a máquina utilizável enquanto você compila e testa. Aplique quand
|
|
|
14
14
|
| Sintoma | Comando | Como ler e o que fazer |
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| Máquina ou editor lento, pouca RAM | `morph-spec doctor --procs` (só lê) | Cada linha diz órfão ou protegido, com a razão. Sem órfão, a pressão vem de fora (vmmem do WSL, IDE): diga isso e passe a sugestão de `.wslconfig` que o relatório traz. Com órfão, mostre a lista e ofereça `--prune`. |
|
|
17
|
-
| Comando voltou `⏳ … não rodou` ou exit 3 | `morph-spec heavy status` | Mostra quem segura cada vaga, há quanto tempo, e a RAM livre. A vaga solta sozinha (processo morto ou timeout). Vaga presa por sessão viva: diga ao usuário quem a segura. Se a causa for RAM e nenhuma vaga estiver presa, rode `doctor --procs
|
|
18
|
-
|
|
|
17
|
+
| Comando voltou `⏳ … não rodou` ou exit 3 | `morph-spec heavy status` | Mostra quem segura cada vaga, há quanto tempo, e a RAM livre. A vaga solta sozinha (processo morto ou timeout). Vaga presa por sessão viva: diga ao usuário quem a segura. Se a causa for RAM e nenhuma vaga estiver presa, rode `doctor --procs`: as stacks paradas a fila já derrubou sozinha. |
|
|
18
|
+
| O Claude Code encerrou um comando de fundo por pouca memória | o mesmo `morph-spec verify … --detach` | O `--detach` não morre com a sessão: repetir retoma a rodada. Outro comando pesado não se repete por conta própria: avise o usuário. |
|
|
19
|
+
| Várias sessões abertas, testando ao mesmo tempo | nada: a fila resolve | São 2 vagas e uma suíte de `verify` por vez. O `verify --detach` espera a vez (até `heavy.detachedWaitMinutes`), então espera longa é fila, não problema: não cancele nem peça ao usuário. Abaixo do piso, a fila derruba sozinha as stacks ociosas; `heavy reclaim` faz o mesmo à mão. |
|
|
19
20
|
| Worktree sobrando ou fora do contêiner | `morph-spec doctor --worktrees` | Lista cada árvore com a razão; remova só por `worktree remove`. |
|
|
20
21
|
| A worktree precisa de arquivo fora do git (`.env`, segredos) | `.worktreeinclude` na raiz do repo | Um padrão por linha, sintaxe do gitignore. Copia só o que casa **e** é ignorado; nunca `node_modules`, build ou `.git`. O `setup` e o `provision` aplicam. |
|
|
21
22
|
| Editor pesado por causa das worktrees | `worktree.root` em `.morph/config/config.json` | Põe as worktrees fora da árvore aberta (caminho absoluto fora do projeto). `init` e `update` já gravam as exclusões no `.vscode/settings.json`. |
|
|
@@ -10,9 +10,9 @@ Por que fresco: o que você sabe da implementação é o ponto cego que eles exi
|
|
|
10
10
|
|
|
11
11
|
## 2. Prova da suíte — uma rodada
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A rodada afetada do §3 da `SKILL.md`, verde, é o que abre o dispatch. A completa vem depois dos relatórios (§6), uma vez só: assim uma correção do Gate 3 não paga duas rodadas cheias, e o que a completa encontrar ainda passa pela confirmação.
|
|
14
14
|
|
|
15
|
-
`flaky` no resultado do verify → a lista vai ao verificador.
|
|
15
|
+
`flaky` no resultado do verify → a lista vai ao verificador. Com `e2eChecks`, `morph-spec e2e status {feature} --json` → `baseUrl` vai ao verificador.
|
|
16
16
|
|
|
17
17
|
## 3. Dispatch — os três na mesma resposta
|
|
18
18
|
|
|
@@ -43,12 +43,14 @@ Não rode build nem teste enquanto um agente está `running`. Com todos de volta
|
|
|
43
43
|
|
|
44
44
|
## 5. Bloqueadores
|
|
45
45
|
|
|
46
|
-
O avaliador também erra: **reproduza cada bloqueador antes de editar**. Não se reproduz → é nota sua no `report.md`, e o próximo dispatch de confirmação recebe a contra-evidência. Confirmado → corrija, commite, `verify {feature}
|
|
46
|
+
O avaliador também erra: **reproduza cada bloqueador antes de editar**. Não se reproduz → é nota sua no `report.md`, e o próximo dispatch de confirmação recebe a contra-evidência. Confirmado → corrija, commite, `verify {feature} --affected`, foto `--before-eval` nova.
|
|
47
47
|
|
|
48
48
|
Confirmação = **dispatch novo** só dos papéis em `gate3.redispatch`, com o `sha` novo e o escopo da rodada: quem abriu bloqueadores confirma só esses (verificador: "confirme só C2, C5"); os outros julgam só o delta `{sha anterior}..{sha novo}`. Papel com SHIP limpo não volta se o delta ficar nos arquivos citados pelos bloqueadores ou em teste; a segurança, se o delta não tocar caminho sensível. Ninguém reavalia a feature inteira. O relatório é append-only e vale o último `**Verdict:**`; o `approve` recusa quando o código andou depois de um `**Reviewed-SHA:**`, salvo esse caso, com quem abriu bloqueador confirmando o `HEAD`.
|
|
49
49
|
|
|
50
50
|
## 6. Gate 3
|
|
51
51
|
|
|
52
|
+
**A rodada completa:** com os relatórios sem bloqueador, `morph-spec verify {feature} --ci --json --detach` em `run_in_background` (sem CI, roda local e diz por quê; repetir o comando retoma a rodada). É a única. Sem `--filter`, sempre, e sem `--affected`: verde filtrado é verde sobre código não testado, e é ela que o gate lê. `reason: "stamp-reused"` = nada andou desde o último verde: vale. Saída `CI rodando` → espere a notificação. `fail` → corrija (CI: `gh run view <id> --log-failed`) e siga o §5, porque a correção é código que ninguém revisou. Nó `e2e` em `skip` → o `gate-check` decide se dispensa (`classifyE2eSkip`, em `risk-detector.js`: docker indisponível pausa sozinho).
|
|
53
|
+
|
|
52
54
|
`morph-spec gate-check {feature} review [--json]` lê os relatórios, a cobertura dos `Cn` e os demais sinais. Forçar passagem apesar da pausa: `morph-spec gate-decision {feature} review --pause false --reason "…"`.
|
|
53
55
|
|
|
54
56
|
Pausa com `resolver: "session"` não é pergunta: faça o que a `reason` diz e rode o `gate-check` de novo. Verify vermelho ou velho → corrija e `verify {feature}`; e2e pulado a pedido → `verify {feature}` sem `--skip-e2e`; bloqueador ou `Cn` não provado → §5; relatório ausente ou sem seção → dispatch. Vira `human` sozinho depois de `FIX_ATTEMPTS` verify vermelhos seguidos ou rodadas de review: a disputa é do usuário.
|