@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.
- package/README.md +3 -7
- package/bin/morph-spec.js +1 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +0 -1
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +3 -4
- package/framework/docs/CLI.md +23 -20
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/next-step.js +2 -2
- package/framework/phases.json +4 -4
- package/framework/schemas/config.schema.json +20 -1
- package/framework/schemas/feature.schema.json +1 -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 +5 -5
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/review.md +5 -5
- package/framework/skills/core/morph-plan/SKILL.md +4 -4
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +7 -3
- package/src/core/state/state-manager.js +0 -24
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +98 -23
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
package/framework/phases.json
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"One
|
|
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.
|
|
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.
|
|
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.
|
|
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`)
|
|
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`:
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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**
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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);
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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 —
|
|
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
|
-
}
|
package/src/lib/heavy/cleanup.js
CHANGED
|
@@ -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,
|