@polymorphism-tech/morph-spec 10.1.1 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -7
  2. package/bin/morph-spec.js +1 -41
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -4
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/agents.json +0 -1
  7. package/framework/commands/morph-archive.md +1 -1
  8. package/framework/commands/morph-preflight.md +3 -4
  9. package/framework/docs/CLI.md +23 -20
  10. package/framework/hooks/README.md +2 -2
  11. package/framework/hooks/claude-code/core/validate-completion.js +0 -35
  12. package/framework/hooks/shared/e2e-check.js +44 -7
  13. package/framework/hooks/shared/heavy-command.js +12 -1
  14. package/framework/hooks/shared/next-step.js +2 -2
  15. package/framework/phases.json +4 -4
  16. package/framework/schemas/config.schema.json +20 -1
  17. package/framework/schemas/feature.schema.json +1 -0
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  20. package/framework/skills/core/morph-apply/SKILL.md +5 -5
  21. package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
  22. package/framework/skills/core/morph-apply/references/review.md +5 -5
  23. package/framework/skills/core/morph-plan/SKILL.md +4 -4
  24. package/framework/skills/core/morph-uiux/SKILL.md +2 -2
  25. package/framework/templates/examples/design-system-examples.md +0 -3
  26. package/package.json +1 -1
  27. package/src/commands/project/doctor.js +0 -1
  28. package/src/commands/state/approve.js +9 -109
  29. package/src/commands/state/report-check.js +44 -2
  30. package/src/commands/validation/verify.js +7 -3
  31. package/src/core/state/state-manager.js +0 -24
  32. package/src/lib/heavy/cleanup.js +8 -2
  33. package/src/lib/plan/critique.js +23 -3
  34. package/src/lib/review-confirmation.js +67 -0
  35. package/src/lib/review-reports.js +1 -0
  36. package/src/lib/risk-detector.js +2 -2
  37. package/src/lib/tasks/test-nodes.js +5 -0
  38. package/src/lib/tasks/test-runner.js +37 -43
  39. package/src/lib/telemetry/telemetry-reader.js +1 -2
  40. package/src/lib/telemetry/verify-event.js +7 -0
  41. package/src/lib/validators/verify-cache.js +63 -0
  42. package/src/lib/validators/verify-runner.js +98 -23
  43. package/src/utils/agents-installer.js +2 -2
  44. package/src/utils/hooks-installer.js +0 -20
  45. package/bin/validate.js +0 -212
  46. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  47. package/src/commands/observability/telemetry.js +0 -60
  48. package/src/commands/validation/validate-feature.js +0 -337
@@ -8,7 +8,7 @@
8
8
  "displayName": "Plan",
9
9
  "goals": [
10
10
  "Understand the business problem and scan the stack (brownfield) or assume the default (greenfield)",
11
- "Write plan.md — scenarios with proof, decisions, reuse and slices",
11
+ "Write plan.md — scenarios with proof, decisions and reuse",
12
12
  "Run plan-check to produce checks.json and reach the plan gate"
13
13
  ],
14
14
  "requiredOutputs": [
@@ -52,9 +52,9 @@
52
52
  "folder": "3-implement",
53
53
  "displayName": "Implementation",
54
54
  "goals": [
55
- "Execute the slices of plan.md, in parallel waves when plan-check allows",
56
- "Close a slice only on verify green and its scenarios proven",
57
- "One report per slice (3-implement/reports/F{n}.md, with **Arquivos:** and **Standards:**) — the implementation record"
55
+ "Implement the whole plan in this session, in sequence",
56
+ "Mark a scenario `feito` only with its evidence; close the phase on a green, unfiltered verify",
57
+ "One row per scenario in 3-implement/report.md — the implementation record"
58
58
  ],
59
59
  "requiredOutputs": [],
60
60
  "optionalOutputs": [
@@ -59,6 +59,11 @@
59
59
  "minimum": 0,
60
60
  "description": "Recorded baseline, kept only for `node-test` and `evals` (ignored, and removed by --record-baseline, on the other runners). Written ONLY by `morph-spec verify --record-baseline`, from a green run — never typed by hand."
61
61
  },
62
+ "parallel": {
63
+ "type": "boolean",
64
+ "default": false,
65
+ "description": "Run this node at the same time as the other nodes with parallel: true. Declare it only on suites that share no database, port or build output: nothing in the harness can check that. Ignored on an e2e: true node and in a round chasing a red suite."
66
+ },
62
67
  "timeoutMs": {
63
68
  "type": "integer",
64
69
  "minimum": 1,
@@ -122,8 +127,14 @@
122
127
  "e2e": {
123
128
  "type": "object",
124
129
  "additionalProperties": true,
125
- "description": "The verification stack of `e2e up` and of the verify e2e node. Only hostServices and resetCommand are described here; the other keys (auth, command, portEnvNames, ...) stay open.",
130
+ "description": "The verification stack of `e2e up` and of the verify e2e node. Only specTimeoutMs, hostServices and resetCommand are described here; the other keys (auth, command, portEnvNames, ...) stay open.",
126
131
  "properties": {
132
+ "specTimeoutMs": {
133
+ "type": "number",
134
+ "exclusiveMinimum": 0,
135
+ "default": 300000,
136
+ "description": "Limit of each Playwright spec run of the verify e2e node, in ms."
137
+ },
127
138
  "resetCommand": {
128
139
  "type": "string",
129
140
  "minLength": 1,
@@ -216,6 +227,14 @@
216
227
  }
217
228
  }
218
229
  },
230
+ "finish": {
231
+ "type": "object",
232
+ "description": "How a finished feature reaches the default branch.",
233
+ "properties": {
234
+ "mode": { "type": "string", "enum": ["pr", "merge"], "default": "pr" }
235
+ },
236
+ "additionalProperties": false
237
+ },
219
238
  "heavy": {
220
239
  "type": "object",
221
240
  "additionalProperties": false,
@@ -131,6 +131,7 @@
131
131
  },
132
132
  "additionalProperties": false
133
133
  },
134
+ "flaky": { "type": "array", "items": { "type": "string" }, "description": "Nodes or e2e specs that were red and turned green with no code change on the way to this stamp. Absent when none. Evidence for the verifier, never a gate input." },
134
135
  "e2eReason": { "type": ["string", "null"], "description": "The LITERAL reason the e2e node skipped (e2e-check.js's own wording), or null when it did not skip. risk-detector.js's classifyE2eSkip() reads it: 'project does not containerize' / 'does not touch runtime' are legitimate dispensations and pass; 'docker is not available' / 'requested' (--skip-e2e) / anything unclassifiable — null included — are an absence of proof and pause Gate 3. A skip recorded WITHOUT a reason is refused on purpose: a nameless dispensation is a silent no-op." },
135
136
  "sha": { "type": ["string", "null"], "description": "HEAD commit SHA at verify time (git rev-parse HEAD); null when not in a git repo or no commits yet." },
136
137
  "ts": { "type": "string", "format": "date-time", "description": "Server-side timestamp, same instant as the corresponding verifications entry's updatedAt." },
@@ -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.1.1"
7
+ cliVersion: "10.2.0"
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.1.1"
7
+ cliVersion: "10.2.0"
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.1.1"
7
+ cliVersion: "10.2.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -24,7 +24,7 @@ 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
- - Ao fechar um `Cn`, atualize a linha dele no `report.md` (`feito` + a evidência: teste, `arquivo:linha`) e commite (`wip(<f>): C2 …`). Decisão ou fato novo → uma linha em `## Notas`.
27
+ - Ao fechar um `Cn`, atualize a linha dele no `report.md` (`feito` + a evidência: 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
28
  - Mídia gerada por IA: `references/media.md`. Erro difícil: `references/debugging.md`.
29
29
 
30
30
  `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 +40,9 @@ O plano inteiro, nesta sessão, em sequência. Sem sub-agent para implementar: o
40
40
 
41
41
  ## 3. Verify, sem filtro
42
42
 
43
- Todo `Cn` `feito`: `morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token), commite tudo e rode `morph-spec verify {feature} --json` sem filtro: a suíte inteira é a prova que o Gate 3 e o CI leem; com `--filter`, o verde mede só um nó. Passou de 10 min: `run_in_background` e espere a notificação **no mesmo turno**. Nó `node-test`/`evals` sem `expectedCount`, ou teste apagado de propósito: `--record-baseline` nesta mesma rodada.
43
+ Todo `Cn` `feito`: commite tudo e rode `morph-spec verify {feature} --json` sem filtro: a suíte inteira é a prova que o Gate 3 e o CI leem; com `--filter`, o verde mede só um nó. Sempre em `run_in_background`, esperando a notificação **no mesmo turno**, sem ler o log em laço. Nó `node-test`/`evals` sem `expectedCount`, ou teste apagado de propósito: `--record-baseline` nesta mesma rodada.
44
44
 
45
- Vermelho → corrija com `verify.feedback` e rode de novo. `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?").
45
+ Vermelho → corrija com `verify.feedback` e rode de novo: 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
46
 
47
47
  ## 4. Ramo por tipo
48
48
 
@@ -55,7 +55,7 @@ Verify verde: siga `references/review.md` — foto `--before-eval`, verificador
55
55
 
56
56
  ## 6. Fechar
57
57
 
58
- Commite o que faltar (relatórios do review) — o `finish` exige árvore limpa. Modo por `AskUserQuestion` (header "Integração"): `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/recap.html`, abra no navegador; `morph-spec recap {feature} --piper` narra com o Piper local).
58
+ `morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/recap.html`, abra no navegador; `morph-spec recap {feature} --piper` narra com o Piper local).
59
59
 
60
60
  ## Arquivo de teste
61
61
 
@@ -28,7 +28,7 @@ A mudança mínima que tira a falha do ar — refatoração e hardening fora do
28
28
 
29
29
  ## 5. Gate 3 — sempre humano
30
30
 
31
- Escreva você `4-review/verifier.md`: `**Verdict:** SHIP|FIX`, `**Reviewed-SHA:** {git rev-parse HEAD}`, `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, evidência = saída do verify), `## Bloqueadores` (`Nenhum.` vale). `morph-spec gate-check {feature} review` pausa sempre. `AskUserQuestion`, header "Gate 3":
31
+ Escreva você `4-review/verifier.md`: `**Verdict:** SHIP|FIX`, `**Reviewed-SHA:** {git rev-parse HEAD}`, `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, evidência = saída do verify), `## Bloqueadores` (`Nenhum.` vale). `morph-spec gate-check {feature} review` pausa sempre. Hotfix não despacha a revisão de segurança: se o diff toca caminho sensível (auth, segredo, pagamento, migration), diga isso ao humano no gate, porque a atenção é dele. `AskUserQuestion`, header "Gate 3":
32
32
 
33
33
  ```
34
34
  question: "Gate 3 — Hotfix de {feature} pronto para ship?"
@@ -12,21 +12,21 @@ Por que fresco: o que você sabe da implementação é o ponto cego que eles exi
12
12
 
13
13
  Commite e rode `morph-spec verify {feature} --ci --json` em `run_in_background` (sem CI, roda local e diz por quê). Sem `--filter`, sempre: verde filtrado é verde sobre código não testado. `reason: "stamp-reused"` = nada andou desde o último verde: vale. Saída `CI rodando` → despache (passo 3) sem esperar o fim; `verify local` → espere, a máquina é uma só.
14
14
 
15
- Nó `e2e` `pass` → `morph-spec e2e status {feature} --json` → `baseUrl` vai ao verificador. `fail` → correção (CI: `gh run view <id> --log-failed`). `skip` → o `gate-check` decide se dispensa (`classifyE2eSkip`, em `risk-detector.js`: docker indisponível pausa sozinho, sem `gate-decision` à mão).
15
+ `flaky` no resultado do verify → a lista vai ao verificador. Nó `e2e` `pass` → `morph-spec e2e status {feature} --json` → `baseUrl` vai ao verificador. `fail` → correção (CI: `gh run view <id> --log-failed`). `skip` → o `gate-check` decide se dispensa (`classifyE2eSkip`, em `risk-detector.js`: docker indisponível pausa sozinho, sem `gate-decision` à mão).
16
16
 
17
17
  ## 3. Dispatch — os três na mesma resposta
18
18
 
19
- `morph-spec report-check {feature} --before-eval --json` tira a foto da árvore e diz `reviewModels.verifier.model` (opus com prompt/modelo ou mídia) e `reviewModels.security.required`. Anote `base` (merge-base com a default) e `sha` (`git rev-parse HEAD`). O revisor abre pela árvore do que mudou (`morph-spec graph diff --base {base} --json`, que ele mesmo roda). Blast radius para o revisor: por símbolo público do diff, `morph-spec graph affected <símbolo> --prod --json`; sem grafo (`no-graph`/`not-found`/`ambiguous`), diga isso a ele — ele cai para `git grep`.
19
+ `morph-spec report-check {feature} --before-eval --json` tira a foto da árvore e diz `reviewModels.verifier.model` (opus com prompt/modelo ou mídia), `reviewModels.security.required` e, em `gate3`, `base`, `sha`, `flaky` e `e2eChecks`. `e2eChecks` não vazio sem `baseUrl`: suba a stack com `morph-spec e2e up {feature}` antes do dispatch; `Cn` sem stack é pendência sua, não rodada de FIX. O revisor calcula o próprio blast radius (`morph-spec graph diff --base {base} --json`, `graph affected`; sem grafo, `git grep`).
20
20
 
21
21
  Despache **sem nome** no dispatch (com nome, o `agent_type` que o hook `test-budget` lê vira o nome e o guard não reconhece o papel). `chore`: só o revisor (∥ segurança).
22
22
 
23
23
  ```
24
24
  Agent (subagent_type: "morph-verifier", model: {reviewModels.verifier.model}):
25
- feature; o conteúdo de 2-plan/checks.json; baseUrl (ou "stack fora do ar: <motivo>");
25
+ feature; o conteúdo de 2-plan/checks.json; baseUrl; gate3.flaky, se houver;
26
26
  reset entre fluxos: `morph-spec e2e reset {feature}`; 1-design/mockups.md se existir;
27
27
  grave .morph/features/{feature}/4-review/verifier.md
28
28
  Agent (subagent_type: "morph-reviewer"):
29
- feature, base, sha, blast radius (ou a ausência);
29
+ feature, base, sha;
30
30
  grave .morph/features/{feature}/4-review/reviewer.md
31
31
  Agent (subagent_type: "morph-security"): ← só com security.required
32
32
  feature, base, sha, reviewModels.security.reasons;
@@ -45,7 +45,7 @@ Não rode build nem teste enquanto um agente está `running`. Com todos de volta
45
45
 
46
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}` local, foto `--before-eval` nova.
47
47
 
48
- Confirmação = **dispatch novo** de cada papel exigido, 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}`. `security.required` virou `true` agora → despache a segurança. 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 qualquer `**Reviewed-SHA:**`.
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
 
@@ -4,7 +4,7 @@ description: Leva um pedido ou o link de uma issue ao plano aprovado — roteia
4
4
  version: core
5
5
  when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "/morph-plan <link da issue>", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
6
6
  user-invocable: true
7
- cliVersion: "10.1.1"
7
+ cliVersion: "10.2.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -44,7 +44,7 @@ Entra na árvore só o que muda escopo, comportamento ou arquitetura e a varredu
44
44
 
45
45
  ## 4. Reuso
46
46
 
47
- Por conceito central (entidade, DTO, componente, hook, helper): `morph-spec graph explain <nome> --json` e `graph affected <nome> --uses-only --json` (`--uses-only`: o grep conta herança como uso). Sem grafo, `grep -rn` e anote que a contagem veio do grep. O veredito vai na tabela `## Reuso`.
47
+ Por conceito central que o plano pretende criar (entidade, DTO, componente, hook, helper): `morph-spec graph explain <nome> --json` e `graph affected <nome> --uses-only --json` (`--uses-only`: o grep conta herança como uso). Sem grafo, `grep -rn` e anote que a contagem veio do grep. O veredito vai na tabela `## Reuso`.
48
48
 
49
49
  ## 5. plan.md
50
50
 
@@ -71,7 +71,7 @@ Plano bom cabe em duas páginas. `morph-spec plan-check {feature}` até zero err
71
71
 
72
72
  Cria ou altera UI → `morph-spec advance {feature} --design` (sempre por ele, nunca criando `1-design/` à mão: o `--design` semeia o `mockups.md`) e `Skill(morph-uiux)` depois do gate; senão `advance {feature}` leva a implement. Decidido uma vez, aqui.
73
73
 
74
- Antes do gate, em `feature` e `bug`: a crítica em contexto fresco de `references/critique.md` (o `gate-check` cobra o `2-plan/critica.md` do plano atual).
74
+ Antes do gate, em `feature` e `bug`: a crítica em contexto fresco de `references/critique.md` (o `gate-check` cobra o `2-plan/critica.md` do plano atual). Plano pequeno dispensa, e quem decide é o código: rode o `gate-check` primeiro; sem sinal `plan-critique`, não despache. Pequeno = até 3 cenários, todos `prova: teste` ou `leitura`, sem `degrau` e sem mídia gerada.
75
75
 
76
76
  `morph-spec gate-check {feature} plan [--json]`. Entrevista que zerou a árvore já é a aprovação do usuário: não repause, salvo a pausa de `signal: "rung"`, que vence e pergunta nomeando cada `Cn` e o seu degrau. Encerrada antes, com nós em `## Fora de escopo`: `morph-spec gate-decision {feature} plan --pause true --reason "entrevista encerrada com nós abertos" --signals '{"discoveryMode":"interview"}'`, e a pergunta do gate lista esses nós.
77
77
 
@@ -79,7 +79,7 @@ Antes do gate, em `feature` e `bug`: a crítica em contexto fresco de `reference
79
79
  - `resolver === "human"` → abra `2-plan/plan.html` no navegador do usuário (`start`/`open`/`xdg-open`; o `plan-check` o gera), o entendimento (problema, fora de escopo, cenários, riscos) e `AskUserQuestion`, header "Gate plano": `"Gate do plano — Plano de {feature} aprovado?"` com `"aprovado, prosseguir" | "preciso ajustar" | "cancelar feature"`; a pergunta lista `## Padrões assumidos` para veto por número; só a decisão sensível que você tomou sozinho vira pergunta antes, na forma de `references/interview.md` (`## Forma da pergunta`). Headless: `morph-spec approve {feature} plan --approver <quem>`.
80
80
  - `pause === false` → uma linha e `morph-spec advance {feature}` (ou `--design`).
81
81
 
82
- Aprovado: "Plano aprovado. Rode `/morph-apply {feature}`.", seguido de `## Padrões assumidos` numerado e "veta N antes do /morph-apply". Veto → `## Errata` com "Padrão N vetado: <valor novo>"; a implementação segue a errata. Não comece a implementar (exceção: `chore`). Rodou `stack-set` nesta sessão: "…numa sessão nova", porque as rules de domínio só carregam no início de uma sessão.
82
+ Aprovado: "Plano aprovado.", com `## Padrões assumidos` numerado, e siga direto com `Skill(morph-apply)`, sem esperar resposta. Veto, a qualquer momento: `## Errata` com "Padrão N vetado: <valor novo>"; a implementação segue a errata. Rodou `stack-set` nesta sessão: pare em "Plano aprovado. Rode `/morph-apply {feature}` numa sessão nova", porque as rules de domínio só carregam no início de uma sessão.
83
83
 
84
84
  ## 7. Deltas por tipo
85
85
 
@@ -4,7 +4,7 @@ description: Conduz a fase de UI/UX de uma feature com plano aprovado — render
4
4
  version: core
5
5
  when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design), antes de /morph-apply
6
6
  user-invocable: true
7
- cliVersion: "10.1.1"
7
+ cliVersion: "10.2.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -22,7 +22,7 @@ A régua de sistema e de estética chega sozinha: a rule `ui-design` carrega qua
22
22
 
23
23
  ## 2. Opções — o usuário escolhe vendo, não lendo
24
24
 
25
- Para cada tela nova ou alterada, `1-design/opcoes/{tela}.html`: **uma página com 2 ou 3 direções distintas**, alternáveis por abas, cada uma com:
25
+ Ajuste em tela existente pula esta seção: uma passagem do protótipo (§3) basta. Para cada tela nova, `1-design/opcoes/{tela}.html`: **uma página com 2 ou 3 direções distintas**, alternáveis por abas, cada uma com:
26
26
 
27
27
  - o conceito em uma frase e o que a torna memorável, no topo da aba;
28
28
  - desktop (1280) e celular (390) lado a lado em moldura de aparelho, com conteúdo realista (nunca lorem ipsum nem métrica inventada);
@@ -50,9 +50,6 @@
50
50
  - *--loading : Estado de loading
51
51
  - *--error : Estado de erro
52
52
 
53
- Validacao:
54
- morph-spec validate-css src/MyApp.Web
55
-
56
53
  =========================================================================== */
57
54
 
58
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "10.1.1",
3
+ "version": "10.2.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -80,7 +80,6 @@ const REQUIRED_COMMAND_FILES = [
80
80
  'src/commands/project/status.js',
81
81
  'src/commands/project/worktree.js',
82
82
  'src/commands/state/approve.js',
83
- 'src/commands/validation/validate-feature.js',
84
83
  'src/commands/scaffold/scaffold.js',
85
84
  'src/commands/mcp/mcp-setup.js',
86
85
  'src/commands/context/context-refresh.js',
@@ -1,9 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { execFileSync } from 'child_process';
4
- import { existsSync } from 'fs';
5
- import { join } from 'path';
6
- import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
4
+ import { getFeature, setApprovalGate, getApprovalGate } from '../../core/state/state-manager.js';
7
5
  import { unmetPrereqs } from '../../core/state/gate-order.js';
8
6
  import { reviewReports, reviewFileName, REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN } from '../../lib/review-reports.js';
9
7
  import { shouldPauseGate } from '../../lib/risk-detector.js';
@@ -12,6 +10,7 @@ import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
12
10
  import { readTrustFromConfig } from './gate-decision.js';
13
11
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
14
12
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
13
+ import { reviewConfirmation } from '../../lib/review-confirmation.js';
15
14
  import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
16
15
  import { reportDotnetIgnores } from './advance.js';
17
16
 
@@ -31,6 +30,11 @@ function reviewStaleness(reports) {
31
30
  return { status: 'skip' };
32
31
  }
33
32
 
33
+ const confirmation = reviewConfirmation(cwd, reports, head);
34
+ const excused = new Set(
35
+ confirmation.confirmed ? confirmation.moved.filter((m) => m.skippable).map((m) => m.role) : [],
36
+ );
37
+
34
38
  let unstamped = null;
35
39
  for (const [role, report] of Object.entries(reports)) {
36
40
  if (report.status === 'missing') continue;
@@ -48,7 +52,7 @@ function reviewStaleness(reports) {
48
52
  unstamped ??= { status: 'unstamped', file, head };
49
53
  continue;
50
54
  }
51
- if (!treeMovedSince(cwd, reviewed, head)) continue;
55
+ if (excused.has(role) || !treeMovedSince(cwd, reviewed, head)) continue;
52
56
  let ahead = 0;
53
57
  try {
54
58
  ahead = Number(execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' }).toString().trim()) || 0;
@@ -218,7 +222,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
218
222
  for (const role of unusable) {
219
223
  const { status } = reports[role];
220
224
  const what = status === 'missing' ? 'não existe'
221
- : status === 'partial' ? 'está PARCIAL — retome o mesmo agente por SendMessage'
225
+ : status === 'partial' ? 'está PARCIAL — despache o mesmo papel de novo dizendo o que já está no relatório'
222
226
  : `não tem \`${VERDICT_TOKEN} SHIP|FIX\``;
223
227
  console.error(chalk.gray(` ${reviewFileName(role)} ${what}.`));
224
228
  }
@@ -280,110 +284,6 @@ program
280
284
  .description('Approve a phase gate to allow workflow progression')
281
285
  .action(approveCommand);
282
286
 
283
- export async function approvalStatusCommand(featureName, options = {}) {
284
- try {
285
- const feature = getFeature(featureName);
286
- if (!feature) {
287
- console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
288
- process.exit(1);
289
- }
290
-
291
- // Phase derived from the filesystem + approved gates, never "unknown".
292
- const featurePath = join(process.cwd(), '.morph', 'features', featureName);
293
- const phase = feature.phase || derivePhase(featurePath);
294
-
295
- // The uiux branch is optional: a feature that never took it has no
296
- // 1-design/ folder and no uiux approval — that is "n/a", not "rejected".
297
- const uiuxGate = getApprovalGate(featureName, 'uiux');
298
- const uiuxTaken = uiuxGate?.approved === true || existsSync(join(featurePath, '1-design'));
299
-
300
- if (options.json) {
301
- const status = {
302
- feature: featureName,
303
- phase,
304
- status: feature.status,
305
- uiuxTaken,
306
- gates: {}
307
- };
308
-
309
- for (const gateName of VALID_GATES) {
310
- status.gates[gateName] = getApprovalGate(featureName, gateName) || { approved: false };
311
- }
312
-
313
- console.log(JSON.stringify(status, null, 2));
314
- return;
315
- }
316
-
317
- console.log(chalk.bold(`\n📋 Approval Status for: ${featureName}`));
318
- console.log('━'.repeat(60));
319
-
320
- console.log(chalk.cyan(`\nCurrent Phase: ${phase}`));
321
- console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
322
-
323
- console.log(chalk.bold('Approval Gates:'));
324
-
325
- for (const gateName of VALID_GATES) {
326
- const gateStatus = getApprovalGate(featureName, gateName);
327
-
328
- if (!gateStatus) {
329
- console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Not yet evaluated')}`);
330
- continue;
331
- }
332
-
333
- if (gateName === 'uiux' && !uiuxTaken) {
334
- console.log(` ${chalk.gray('—')} ${gateName}: ${chalk.gray('n/a (ramo de design não tomado)')}`);
335
- continue;
336
- }
337
-
338
- if (gateStatus.approved) {
339
- console.log(` ${chalk.green('✓')} ${gateName}: ${chalk.green('Approved')}`);
340
- if (gateStatus.timestamp) {
341
- console.log(chalk.gray(` Approved at: ${gateStatus.timestamp}`));
342
- }
343
- if (gateStatus.approvedBy) {
344
- console.log(chalk.gray(` Approved by: ${gateStatus.approvedBy}`));
345
- }
346
- } else {
347
- // Distinguish "never evaluated" (initialized approved:false, no
348
- // timestamp) from an explicit rejection/revocation (has timestamp).
349
- const neverEvaluated = !gateStatus.timestamp && !gateStatus.rejectedAt && !gateStatus.revokedAt;
350
- if (neverEvaluated) {
351
- console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Pending (not yet evaluated)')}`);
352
- continue;
353
- }
354
- console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Revoked')}`);
355
- if (gateStatus.reason) {
356
- console.log(chalk.gray(` Reason: ${gateStatus.reason}`));
357
- }
358
- if (gateStatus.timestamp) {
359
- console.log(chalk.gray(` Rejected at: ${gateStatus.timestamp}`));
360
- }
361
- }
362
- }
363
-
364
- const history = getApprovalHistory(featureName);
365
- if (history && history.length > 0) {
366
- console.log(chalk.bold('\nApproval History:'));
367
- history.slice(-5).forEach(entry => {
368
- const status = entry.approved ? chalk.green('✓ Approved') : chalk.red('✗ Rejected');
369
- console.log(` ${status} ${entry.gate} - ${entry.timestamp}`);
370
- });
371
- }
372
-
373
- console.log('\n' + '━'.repeat(60) + '\n');
374
-
375
- } catch (error) {
376
- console.error(chalk.red(`\n❌ Error showing approval status: ${error.message}\n`));
377
- process.exit(1);
378
- }
379
- }
380
-
381
- program
382
- .command('approval-status <feature>')
383
- .alias('status')
384
- .description('Show approval status for all gates')
385
- .action(approvalStatusCommand);
386
-
387
287
  if (import.meta.url === `file://${process.argv[1]}`) {
388
288
  if (process.argv.length > 2) {
389
289
  program.parse(process.argv);
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { execFileSync } from 'child_process';
2
3
  import { existsSync, readFileSync } from 'fs';
3
4
  import { join } from 'path';
4
5
  import { implementProgress, parseReport, reportPath } from '../../../framework/hooks/shared/report-progress.js';
@@ -14,6 +15,8 @@ import { isTestPath } from '../../lib/graph/sanitize.js';
14
15
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
15
16
  import { collectReviewRequirement } from './gate-check.js';
16
17
  import { featureGeneratesMedia } from '../../lib/plan/media.js';
18
+ import { resolveBaseRef } from '../../lib/git/branch-classifier.js';
19
+ import { reviewConfirmation } from '../../lib/review-confirmation.js';
17
20
 
18
21
  /** Each review report: complete when it carries a readable verdict and is not partial. */
19
22
  export function inspectReviewReports(cwd, feature) {
@@ -75,10 +78,44 @@ function treeFor(cwd, feature, beforeEval) {
75
78
  const tree = treeState(cwd, { snapshot });
76
79
  if (!beforeEval) return { tree };
77
80
  const reviewModels = reviewDispatch(cwd, feature);
81
+ const gate3 = gate3Briefing(cwd, feature);
78
82
  const next = snapshotTree(cwd);
79
- if (!next) return { tree, beforeEval: null, reviewModels };
83
+ if (!next) return { tree, beforeEval: null, reviewModels, gate3 };
80
84
  atomicWriteJson(path, next);
81
- return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
85
+ return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels, gate3 };
86
+ }
87
+
88
+ function gitLine(cwd, args) {
89
+ try {
90
+ return execFileSync('git', args, { cwd, stdio: 'pipe' }).toString().trim() || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function readVerification(cwd, feature) {
97
+ try {
98
+ return JSON.parse(readFileSync(join(cwd, '.morph', 'features', feature, 'feature.json'), 'utf8')).verification ?? null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ // What every dispatch needs, in one read. `redispatch: null` = no report yet, every required role goes.
105
+ function gate3Briefing(cwd, feature) {
106
+ const sha = gitLine(cwd, ['rev-parse', 'HEAD']);
107
+ const defaultRef = resolveBaseRef(detectDefaultBranch(cwd), cwd);
108
+ const reports = reviewReports(cwd, feature);
109
+ const anyReport = Object.values(reports).some((report) => report.status !== 'missing');
110
+ const moved = anyReport && sha ? reviewConfirmation(cwd, reports, sha).moved : [];
111
+ const { checks } = readPlanChecks(cwd, feature);
112
+ return {
113
+ base: defaultRef ? gitLine(cwd, ['merge-base', defaultRef, 'HEAD']) : null,
114
+ sha,
115
+ flaky: readVerification(cwd, feature)?.flaky ?? [],
116
+ e2eChecks: (checks?.checks ?? []).filter((check) => check.proof === 'e2e').map((check) => check.id),
117
+ redispatch: anyReport ? moved.filter((role) => !role.skippable).map((role) => role.role) : null,
118
+ };
82
119
  }
83
120
 
84
121
  export function reportCheckCommand(feature, options = {}) {
@@ -128,6 +165,11 @@ function printHuman(obj) {
128
165
  const m = obj.reviewModels;
129
166
  console.log(chalk.gray(` dispatch: verifier ${m.verifier.model}, reviewer ${m.reviewer.model}${m.security.required ? `, security ${m.security.model}` : ''}`));
130
167
  }
168
+ if (obj.gate3) {
169
+ const g = obj.gate3;
170
+ console.log(chalk.gray(` gate 3: base ${g.base?.slice(0, 8) ?? '-'}, sha ${g.sha?.slice(0, 8) ?? '-'}`
171
+ + `${g.redispatch ? `, voltam: ${g.redispatch.join(', ') || 'nenhum'}` : ''}${g.flaky.length ? `, instáveis: ${g.flaky.join(', ')}` : ''}`));
172
+ }
131
173
  printTree(obj.tree);
132
174
  console.log('');
133
175
  }
@@ -109,12 +109,14 @@ const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'],
109
109
  const CI_EXCLUSIVE_FLAGS = [
110
110
  ['filter', '--filter'], ['skipTests', '--skip-tests'], ['skipBuild', '--skip-build'], ['skipE2e', '--skip-e2e'],
111
111
  ['freshE2e', '--fresh-e2e'], ['linux', '--linux'], ['recordBaseline', '--record-baseline'], ['refreshCache', '--refresh-cache'],
112
- ['noPersist', '--no-persist'],
112
+ ['noPersist', '--no-persist'], ['fresh', '--fresh'],
113
113
  ];
114
114
 
115
- async function runLocalVerify(featureName, options) {
115
+ async function runLocalVerify(featureName, options, fingerprint) {
116
116
  try {
117
117
  return await runVerify(process.cwd(), featureName, {
118
+ fingerprint,
119
+ fresh: options.fresh,
118
120
  timeout: options.timeout,
119
121
  skipTests: options.skipTests,
120
122
  skipBuild: options.skipBuild,
@@ -214,11 +216,12 @@ export async function verifyCommand(featureName, options = {}) {
214
216
  // Taken before the run: it is the tree the run is about to read.
215
217
  const fingerprint = treeFingerprint(process.cwd());
216
218
 
217
- const result = ci ? ci.result : await runLocalVerify(featureName, options);
219
+ const result = ci ? ci.result : await runLocalVerify(featureName, options, fingerprint);
218
220
 
219
221
  const cleanup = ci ? { status: 'skipped', reason: 'ci' } : afterHeavyRun({
220
222
  projectPath: process.cwd(),
221
223
  config: readHeavyConfig(process.cwd()),
224
+ onlyUnderPressure: true,
222
225
  });
223
226
 
224
227
  const historyKey = 'verifications.feature';
@@ -254,6 +257,7 @@ export async function verifyCommand(featureName, options = {}) {
254
257
  e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
255
258
  sha: ci ? ci.run.headSha : getHeadSha(process.cwd()),
256
259
  ts: now,
260
+ ...(result.flaky?.length ? { flaky: result.flaky } : {}),
257
261
  ...(ci ? { source: 'ci', ciRun: ci.run.url } : {}),
258
262
  };
259
263
  await updateFeature(featureName, 'verification', stamp);
@@ -521,27 +521,3 @@ export function getApprovalGate(featureName, gate) {
521
521
  return feature.approvalGates[gate] || null;
522
522
  }
523
523
 
524
- export function getApprovalHistory(featureName) {
525
- const state = loadState();
526
- const feature = state.features[featureName];
527
-
528
- if (!feature || !feature.approvalGates) {
529
- return [];
530
- }
531
-
532
- const history = [];
533
-
534
- Object.entries(feature.approvalGates).forEach(([gate, data]) => {
535
- if (data.timestamp) {
536
- history.push({
537
- gate,
538
- approved: data.approved,
539
- timestamp: data.timestamp,
540
- approvedBy: data.approvedBy || data.rejectedBy,
541
- reason: data.reason
542
- });
543
- }
544
- });
545
-
546
- return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
547
- }
@@ -1,4 +1,5 @@
1
1
  import { readdirSync } from 'node:fs';
2
+ import { freemem } from 'node:os';
2
3
  import { getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
3
4
  import { listProcesses, defaultProcExec } from '../procs/list.js';
4
5
  import { readProjectConfig } from '../../../framework/hooks/shared/heavy-config.js';
@@ -8,6 +9,7 @@ const DOTNET_ACTIVE = /\bdotnet(?:\.exe)?["']?\s+(?:build|test|publish|pack|msbu
8
9
  const IDLE_WORKER = /\/nodemode:\d|vbcscompiler|razorserver/i;
9
10
  const SHUTDOWN_TIMEOUT_MS = 30_000;
10
11
  const TAIL_CHARS = 400;
12
+ const GB = 1024 ** 3;
11
13
 
12
14
  function hasDotnet(projectPath) {
13
15
  const config = readProjectConfig(projectPath);
@@ -32,14 +34,18 @@ export function dotnetBusy(procs) {
32
34
  });
33
35
  }
34
36
 
35
- export function afterHeavyRun({ projectPath, config, env = process.env, deps = {} }) {
37
+ export function afterHeavyRun({ projectPath, config, env = process.env, onlyUnderPressure = false, deps = {} }) {
36
38
  const {
37
- exec = defaultProcExec, list = listProcesses, dotnet = hasDotnet, exclusive = runExclusive,
39
+ exec = defaultProcExec, list = listProcesses, dotnet = hasDotnet, exclusive = runExclusive, freeMem = freemem,
38
40
  } = deps;
39
41
 
40
42
  if (!config.enabled) return { status: 'skipped', reason: 'semáforo desligado: sem ele nada prova que outra sessão não está compilando' };
41
43
  if (!config.shutdownBuildServer) return { status: 'skipped', reason: 'desligado em heavy.shutdownBuildServer' };
42
44
  if (!dotnet(projectPath)) return { status: 'skipped', reason: 'projeto sem .NET' };
45
+ // A warm compiler is what makes the next round short; twice the floor keeps the next queue open.
46
+ if (onlyUnderPressure && freeMem() / GB >= config.minFreeMemGb * 2) {
47
+ return { status: 'skipped', reason: 'RAM livre de sobra: o compilador fica quente para a próxima rodada' };
48
+ }
43
49
 
44
50
  const guarded = exclusive({
45
51
  dir: heavyDir(env), label: 'build-server-shutdown', maxHoldMs: config.maxHoldMinutes * 60_000, env,