@polymorphism-tech/morph-spec 8.24.3 → 8.26.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 +2 -2
- package/bin/morph-spec.js +2 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +22 -21
- package/framework/agents.json +1 -1
- package/framework/commands/create-umbraco-lp.md +1 -1
- package/framework/commands/migrate-lp-to-umbraco.md +1 -1
- package/framework/commands/morph-apply.md +4 -4
- package/framework/commands/morph-hotfix.md +3 -3
- package/framework/commands/morph-proposal.md +5 -5
- package/framework/docs/CLI.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/evals/rubrics/test-coverage.md +7 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +115 -4
- package/framework/hooks/shared/payload-utils.js +19 -4
- package/framework/rules/dotnet-templates.md +2 -1
- package/framework/rules/morph-workflow.md +42 -29
- 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 +6 -6
- package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +7 -7
- package/framework/skills/core/morph-implement/SKILL.md +21 -6
- package/framework/skills/core/morph-plan/SKILL.md +15 -3
- package/framework/skills/core/morph-proposal/SKILL.md +7 -7
- package/framework/skills/core/morph-review/SKILL.md +10 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
- package/framework/skills/core/morph-standards/SKILL.md +4 -4
- package/framework/skills/core/morph-uiux/SKILL.md +3 -3
- package/framework/templates/README.md +72 -643
- package/framework/templates/examples/spec-examples.md +2 -1
- package/package.json +1 -1
- package/src/commands/project/claim.js +19 -3
- package/src/commands/state/approve.js +17 -3
- package/src/commands/state/finish.js +202 -15
- package/src/commands/state/gate-check.js +9 -20
- package/src/commands/state/report-check.js +157 -5
- package/src/lib/evaluator-report.js +72 -0
- package/src/lib/risk-detector.js +35 -0
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:** 8.
|
|
6
|
+
**Version:** 8.26.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
368
368
|
|
|
369
369
|
---
|
|
370
370
|
|
|
371
|
-
*morph-spec v8.
|
|
371
|
+
*morph-spec v8.26.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -232,7 +232,7 @@ program
|
|
|
232
232
|
.option('--name <name>', 'Human-readable feature name (default: the slug)')
|
|
233
233
|
.option('--description <text>', 'What/why of the feature — required at birth unless --request is given (it seeds the description)')
|
|
234
234
|
.option('--status <status>', 'Initial feature status (default: in_progress)')
|
|
235
|
-
.option('--type <type>', 'Work type: feature|bug|chore|hotfix (default: feature) —
|
|
235
|
+
.option('--type <type>', 'Work type: feature|bug|chore|hotfix (default: feature) — each type stamps its own gate policy')
|
|
236
236
|
.option('--request <text>', 'Free-text request classified by the deterministic detector; tie/none exits 2 with candidates for an LLM/user decision (feature NOT created)')
|
|
237
237
|
.option('--detect-source <source>', 'workTypeDetection.source recorded when --type is explicit: deterministic|llm|user (default: user)')
|
|
238
238
|
.option('--worktree', 'Create the feature already inside its own git worktree (branch morph/{feature}) — fails open outside a git repo')
|
|
@@ -442,6 +442,7 @@ program
|
|
|
442
442
|
.description('claim [feature] acquires it (moves this tree\'s previous claim in the same step); no args shows the board (claims are advisory warnings, never locks)')
|
|
443
443
|
.option('--release', 'Release the claim on <feature> (recovery: the owning session died)')
|
|
444
444
|
.option('--list', 'Read-only: show claims without acquiring (default when no feature is given)')
|
|
445
|
+
.option('--all', 'Include dead claims (past the 30min TTL) — hidden by default, they no longer warn about anything')
|
|
445
446
|
.option('--json', 'Output as JSON')
|
|
446
447
|
.action((feature, options) => claimCommand(feature, options));
|
|
447
448
|
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -15,9 +15,9 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
15
15
|
|
|
16
16
|
**NUNCA:**
|
|
17
17
|
- Pular para código sem proposal aprovado (Gate 1) e plano aprovado (Gate 2).
|
|
18
|
-
- Modificar o contrato do plano depois do Gate 2 — `spec.md`, `mandate.md`, e os campos `title`/`description`/`dependencies`/`effort`/`doneCriteria`/`group`/`agent`/`standards` de `tasks.json` ficam congelados (`protect-spec-files` bloqueia
|
|
18
|
+
- Modificar o contrato do plano depois do Gate 2 — `spec.md`, `mandate.md`, e os campos `title`/`description`/`dependencies`/`effort`/`doneCriteria`/`group`/`agent`/`standards` de `tasks.json` ficam congelados (`protect-spec-files` bloqueia `Write`/`Edit`); mutáveis: `status`/`outputs`/`notes` (ver SEMPRE). **Fato errado sobre o código não exige revogar gate**: bloco `## Errata` append-only no fim, corpo intacto (MORPH.md §3). **Use cases (`2-plan/usecases/UC-*.md`) NÃO congelam** — editáveis por design.
|
|
19
19
|
- Escrever critérios de aceite no `proposal.md` — pertencem aos use cases (pós-condição **observável em teste**, chega **literal** ao `doneCriteria`).
|
|
20
|
-
- Editar `.morph/specs/usecases/` à mão — só o `finish` escreve lá (lê de `2-plan/usecases/`, carimba `status: implemented`, sobrescreve por ID). Mudar um UC existente (
|
|
20
|
+
- Editar `.morph/specs/usecases/` à mão — só o `finish` escreve lá (lê de `2-plan/usecases/`, carimba `status: implemented`, sobrescreve por ID). Mudar um UC existente (`bug`): copie para `2-plan/usecases/` da feature e corrija lá.
|
|
21
21
|
- Listar perguntas em texto simples — sempre `AskUserQuestion` (1-4 perguntas por chamada).
|
|
22
22
|
- Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), ou SQL cru sem justificativa.
|
|
23
23
|
- Editar `taskScores`, `gateDecisions` ou gates de `feature.json` à mão — sempre via `morph-spec score`/`gate-check`/`approve` (timestamps server-side; Edit manual colide com o hook `state-sync`).
|
package/framework/MORPH.md
CHANGED
|
@@ -150,10 +150,12 @@ Não invente caminhos alternativos.
|
|
|
150
150
|
|
|
151
151
|
- `mandate.md` — contrato de implementação: stack exata, standards obrigatórios (com links), patterns proibidos, **verificação de runtime** (como o stack sobe, que fluxos são exercitados, quais viram spec Playwright, setup de auth) e checklist de done verificável. Lido no início de cada task — específico o bastante para resolver ambiguidades sem consultar o Orchestrator.
|
|
152
152
|
|
|
153
|
-
- `tasks.json` — validada contra
|
|
153
|
+
- `tasks.json` — validada contra `.morph/framework/schemas/tasks.schema.json`. Cada task: `id` (T1, T2...), `title`, `description`, `dependencies` (IDs), `effort` (S/M/L/XL), `doneCriteria`, `status` (pending/in_progress/done/skipped/blocked), `outputs` (paths esperados), `usecase` (opcional — **ID** do use case, `UC-07`, nunca o nome do arquivo). `doneCriteria` de task com `usecase` carrega a **pós-condição literal**, com rótulo (`"P2 — cada veterinário traz..."`). Toda entrada do recap.md referencia um ID do tasks.json.
|
|
154
154
|
|
|
155
155
|
- `decisions.md` — log de decisões técnicas (alternativa descartada, motivo, quem decidiu). Só existe se houver decisão não trivial a rastrear.
|
|
156
156
|
|
|
157
|
+
**Errata em spec/mandate congelados:** congelar o CONTRATO não é congelar um ERRO DE FATO. Se a spec afirma sobre o código algo que o código desmente, acrescente um bloco `## Errata` no FIM do arquivo — corpo intacto, append-only, aceito pelo `protect-spec-files` sem revogar gate. Mudança de ESCOPO continua exigindo revogar o gate e re-aprovar.
|
|
158
|
+
|
|
157
159
|
### Implement (`3-implement/`)
|
|
158
160
|
|
|
159
161
|
- `recap.md` — atualizado ao fim de cada task: ID, o que foi feito, arquivos modificados, problemas e resolução, standard consultado. Categoria sem nada a registrar → escreva "nenhum" (recap vazio é pior que mínimo).
|
|
@@ -184,7 +186,7 @@ Cada fase produz um trace JSON em `.morph/traces/YYYY-MM-DD-{feature}-{phase}.js
|
|
|
184
186
|
|
|
185
187
|
**Nunca carregue todos os standards de uma vez** — custa tokens sem benefício, você não sabe quais serão relevantes antes de começar. Consulte sob demanda, por tarefa.
|
|
186
188
|
|
|
187
|
-
Índice em
|
|
189
|
+
Índice em `.morph/framework/standards/STANDARDS.json` (`id`, `name`, `path`, `category`, `tags`).
|
|
188
190
|
|
|
189
191
|
**Fluxo de consulta:**
|
|
190
192
|
|
|
@@ -241,7 +243,7 @@ Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `
|
|
|
241
243
|
|
|
242
244
|
**Registre quem aprovou (`approvedBy`):** objeto `{ approved, timestamp, approvedBy }`. `"auto"` — política `trust: auto` sem decisão humana. `"manual"` — humano aprovou explicitamente (via `AskUserQuestion`, ou, sem ela em headless, via texto começando em "aprovado"); em `trust: manual` todo gate é `"manual"` — nunca grave `"auto"` num gate que um humano aprovou. Mantém o audit trail honesto nos contextos headless (CI, `claude -p`) onde mais importa.
|
|
243
245
|
|
|
244
|
-
**O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar vs auto-aprovar) bateu com a decisão persistida. **Default:** `gate-check <feature> <gate> [--signals <json>] [--json]` decide via `shouldPauseGate`; `--signals` sobrescreve só o que o disco não expõe sozinho (`designSystemDivergence`, `standardsViolations`, `evalScore`). **Escape manual** (sinal que `shouldPauseGate` nem representa, ex.: modo de descoberta): `gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`. Ambos gravam `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt }` server-side (shape em
|
|
246
|
+
**O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar vs auto-aprovar) bateu com a decisão persistida. **Default:** `gate-check <feature> <gate> [--signals <json>] [--json]` decide via `shouldPauseGate`; `--signals` sobrescreve só o que o disco não expõe sozinho (`designSystemDivergence`, `standardsViolations`, `evalScore`). **Escape manual** (sinal que `shouldPauseGate` nem representa, ex.: modo de descoberta): `gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`. Ambos gravam `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt }` server-side (shape em `.morph/framework/schemas/feature.schema.json`). **Nunca edite `gateDecisions` à mão** — colide com state-sync. Divergência → o guard injeta aviso no contexto; cada conferência registra `coherent`/`divergent` em `.morph/logs/activity.json` — nunca bloqueia.
|
|
245
247
|
|
|
246
248
|
---
|
|
247
249
|
|
|
@@ -268,10 +270,9 @@ Inline (sem dispatch) é aceitável para tasks curtas (S/M) **dentro do domínio
|
|
|
268
270
|
|
|
269
271
|
**NÃO passe:** histórico da conversa principal, contexto de outras tarefas não relacionadas, outputs de ferramentas não pertinentes.
|
|
270
272
|
|
|
271
|
-
**O ARQUIVO é o retorno, a última mensagem é cortesia.**
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
grava o relatório também em um arquivo canônico:
|
|
273
|
+
**O ARQUIVO é o retorno, a última mensagem é cortesia.** O evento de volta do sub-agent às vezes é só
|
|
274
|
+
um `idle_notification` sem texto — a última mensagem **pode não chegar** inteira, por isso não é o
|
|
275
|
+
canal autoritativo. O sub-agent **sempre** grava o relatório num arquivo canônico:
|
|
275
276
|
|
|
276
277
|
- Task de implementação: `.morph/features/{feature}/3-implement/reports/{taskId}.md` (um arquivo por task concluída, mesmo quando uma sessão de sub-agent cobre várias tasks de um fluxo).
|
|
277
278
|
- Avaliador do Gate 3 (`morph-evaluator`): `.morph/features/{feature}/4-review/evaluator-report.md` — relatório BRUTO; a `morph-review` o lê e sintetiza o `review-report.md` do Gate 3 (nunca colidem: um é saída crua do sub-agent, o outro é curadoria do orquestrador).
|
|
@@ -279,14 +280,14 @@ grava o relatório também em um arquivo canônico:
|
|
|
279
280
|
O orquestrador **lê o arquivo** ao retomar controle após o dispatch — nunca confia só na mensagem
|
|
280
281
|
de retorno da tool (que, se chegou inteira, só economiza a leitura).
|
|
281
282
|
|
|
282
|
-
### Protocolo de
|
|
283
|
+
### Protocolo de auditoria do relatório
|
|
283
284
|
|
|
284
|
-
**"Relatório ausente = o agente falhou" é a leitura errada
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
**"Relatório ausente = o agente falhou" é a leitura errada** — ele pode ter feito o trabalho e
|
|
286
|
+
perdido o meio de gravá-lo (ex.: o worktree sumiu debaixo dele). E relatório PRESENTE também não
|
|
287
|
+
basta: um gravado cedo demais fica completo e desatualizado.
|
|
287
288
|
|
|
288
|
-
A pergunta que decide não é "o relatório chegou?", é **"o TRABALHO entrou?"** —
|
|
289
|
-
evidência, não pela palavra do agente:
|
|
289
|
+
A pergunta que decide não é "o relatório chegou?", é **"o TRABALHO entrou, e é isto que ele diz?"** —
|
|
290
|
+
respondida por evidência, não pela palavra do agente:
|
|
290
291
|
|
|
291
292
|
```
|
|
292
293
|
morph-spec report-check <feature> [task] --json
|
|
@@ -294,15 +295,15 @@ morph-spec report-check <feature> [task] --json
|
|
|
294
295
|
|
|
295
296
|
| veredito | o que significa | ação |
|
|
296
297
|
| --- | --- | --- |
|
|
297
|
-
| `ok` | relatório presente
|
|
298
|
+
| `ok` | relatório presente, com os 4 itens, e mais novo que o trabalho | siga o fluxo |
|
|
299
|
+
| `report-stale` | relatório completo mas **VELHO**: a task escreveu arquivos DEPOIS de ele ser gravado (o agente relatou cedo e continuou; ex.: "teste ainda não escrito" com os testes já no disco) | **não re-despache** — leia `observed` (writes reais do `events.jsonl`, não `outputs` auto-declarado) e atualize o relatório a partir dessa evidência |
|
|
298
300
|
| `report-incomplete` | relatório existe, faltam itens (ele lista quais) | complete a partir da mensagem de retorno, carimbe **DEGRADADO** |
|
|
299
301
|
| `work-landed-report-missing` | sem relatório, mas os `outputs` da task estão no disco | **não re-despache** — grave você mesmo o relatório canônico a partir da mensagem de retorno, carimbado **DEGRADADO** |
|
|
300
302
|
| `no-work-no-report` | sem relatório e sem output | re-despache **uma** vez com o mesmo prompt |
|
|
301
303
|
| `report-missing-evidence-unknown` | sem relatório e a task não declarou `outputs` | re-despache (sem evidência, o conservador é o certo) |
|
|
302
304
|
|
|
303
|
-
Sai 0 só em `ok`.
|
|
304
|
-
|
|
305
|
-
escale ao humano.
|
|
305
|
+
Sai 0 só em `ok`. Todo caminho degradado é **registrado** no recap e nas `notes` da task, nunca
|
|
306
|
+
aceito em silêncio. Segunda ausência sem trabalho entregue é falha real: escale ao humano.
|
|
306
307
|
|
|
307
308
|
**O que receber de volta** (no arquivo, e na mensagem final quando ela chega) — os **4 itens obrigatórios** de todo relatório de dispatch:
|
|
308
309
|
- O que foi feito (2-3 frases)
|
|
@@ -325,16 +326,16 @@ Sub-agent deve ser autossuficiente com o que você passou — se precisar de mai
|
|
|
325
326
|
|
|
326
327
|
**Dispatch em paralelo (multi-agent):** fluxos **genuinamente independentes** (ex: backend ‖ frontend, convergindo só no fim) → despache múltiplos sub-agents **ao mesmo tempo**, um por fluxo, em vez de série. `morph-spec dag {feature}` analisa o DAG (`dependencies`/`group`), **pondera o esforço** (raízes/fluxos só de tasks S não ganham dispatch próprio) e imprime a decisão; regras e cap de agentes simultâneos em `morph-apply` ("Análise de DAG"). Colisão de `outputs` entre fluxos → `dag` recomenda `isolation: "worktree"`, cada agente num worktree próprio, orquestrador integra por merge (`morph-apply` §3b-w). Sem colisão: working tree compartilhada, verificação única pós-batch.
|
|
327
328
|
|
|
328
|
-
O padrão de dispatch e o template de prompt para sub-agents estão detalhados em `
|
|
329
|
+
O padrão de dispatch e o template de prompt para sub-agents estão detalhados em `morph-implement` §4.
|
|
329
330
|
|
|
330
331
|
---
|
|
331
332
|
|
|
332
333
|
## 7. Skills disponíveis
|
|
333
334
|
|
|
334
|
-
Skills são documentos de instrução que você invoca sob demanda. Eles ficam em
|
|
335
|
+
Skills são documentos de instrução que você invoca sob demanda. Eles ficam em `.claude/skills/`.
|
|
335
336
|
Não invoque uma skill fora do momento indicado.
|
|
336
337
|
|
|
337
|
-
Path de cada skill:
|
|
338
|
+
Path de cada skill: `.claude/skills/{nome}/SKILL.md`.
|
|
338
339
|
|
|
339
340
|
| Skill | Quando invocar |
|
|
340
341
|
| ----------------------- | --------------------------------------------------------------------------- |
|
|
@@ -352,7 +353,7 @@ Path de cada skill: `framework/skills/core/{nome}/SKILL.md`.
|
|
|
352
353
|
|
|
353
354
|
Slash commands (`/morph-proposal`, `/morph-apply`, `/morph-hotfix` — linhas acima) são atalhos que invocam a `Skill()` correspondente; o agente pode chamá-la direto quando o usuário pede "use /morph-proposal".
|
|
354
355
|
|
|
355
|
-
Cada skill contém instruções passo a passo — leia, siga, não desvie sem justificativa no recap.md. Só
|
|
356
|
+
Cada skill contém instruções passo a passo — leia, siga, não desvie sem justificativa no recap.md. Só as skills **core** do framework são instaladas. Nenhuma skill é invocada proativamente por hook ou script — você decide quando a situação pede o rigor dela.
|
|
356
357
|
|
|
357
358
|
---
|
|
358
359
|
|
package/framework/agents.json
CHANGED
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"tools": "Read, Glob, Grep, Bash",
|
|
148
148
|
"mcp": ["playwright"],
|
|
149
149
|
"model": "opus",
|
|
150
|
-
"persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Você também tem Playwright: quando o nó `e2e` do verify subiu o stack, NAVEGUE a aplicação como um usuário faria e percorra os fluxos da feature — ler o diff prova que o código existe, usar a aplicação prova que ela funciona. Confira console e network atrás de erro que nenhum teste cobre. Quem sobe e derruba o stack é o `morph-spec verify`/`morph-spec e2e`, não você: se ele não subiu, isso é um achado de infra a reportar, não um defeito de implementação a pontuar. Se um fluxo que você exercitou merece virar regressão, recomende a spec Playwright no relatório — recomendar é seu papel, escrever não. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
|
|
150
|
+
"persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Você também tem Playwright: quando o nó `e2e` do verify subiu o stack, NAVEGUE a aplicação como um usuário faria e percorra os fluxos da feature — ler o diff prova que o código existe, usar a aplicação prova que ela funciona. Confira console e network atrás de erro que nenhum teste cobre. Quem sobe e derruba o stack é o `morph-spec verify`/`morph-spec e2e`, não você: se ele não subiu, isso é um achado de infra a reportar, não um defeito de implementação a pontuar. Se um fluxo que você exercitou merece virar regressão, recomende a spec Playwright no relatório — recomendar é seu papel, escrever não. Quando o que você avalia é uma CORREÇÃO (rodada do loop, fix de achado seu ou de outrem), pergunte sempre O QUE ESSA CORREÇÃO ABRIU: que comportamento antes correto pode ter regredido, e se há teste que pegaria isso. Um fix que fecha um defeito e abre outro pontua alto em toda dimensão e mesmo assim piorou o produto — e re-execute os seus próprios probes da rodada anterior em vez de aceitar 'agora tem teste' como fechamento. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
|
|
151
151
|
},
|
|
152
152
|
"scout": {
|
|
153
153
|
"title": "Codebase Scout",
|
|
@@ -29,7 +29,7 @@ Cria um projeto de landing page novo a partir do template canônico `MORPH_Umbra
|
|
|
29
29
|
|
|
30
30
|
## Referências
|
|
31
31
|
|
|
32
|
-
-
|
|
32
|
+
- `.claude/skills/create-umbraco-lp/SKILL.md`
|
|
33
33
|
- `docs/umbraco-lp-patterns.md` (em `main`)
|
|
34
34
|
|
|
35
35
|
**Projeto:** $ARGUMENTS
|
|
@@ -26,7 +26,7 @@ Migra a LP de origem para o template `MORPH_Umbraco_LP_Template` de forma reprod
|
|
|
26
26
|
|
|
27
27
|
## Referências
|
|
28
28
|
|
|
29
|
-
-
|
|
29
|
+
- `.claude/skills/migrate-lp-to-umbraco/SKILL.md` — playbook completo (passos 0-6, gate de paridade)
|
|
30
30
|
- `polymorphism-tech/MORPH_Umbraco_LP_Template` — template canônico (privado, branch `master`), obtido no passo 0
|
|
31
31
|
- `MORPH_Umbraco_LP_Template/scripts/new-lp.sh` / `new-lp.ps1` — scaffold
|
|
32
32
|
|
|
@@ -31,9 +31,9 @@ Se algum estiver faltando, abortar e pedir ao usuário para rodar `/morph-propos
|
|
|
31
31
|
|
|
32
32
|
## Referências
|
|
33
33
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
34
|
+
- `.morph/framework/MORPH.md` — charter completo
|
|
35
|
+
- `.claude/skills/morph-implement/SKILL.md`
|
|
36
|
+
- `.claude/skills/morph-eval/SKILL.md`
|
|
37
|
+
- `.claude/skills/morph-review/SKILL.md`
|
|
38
38
|
|
|
39
39
|
**Feature:** $ARGUMENTS
|
|
@@ -32,8 +32,8 @@ Resolve uma queda ou bug crítico de produção com prioridade máxima em veloci
|
|
|
32
32
|
|
|
33
33
|
## Referências
|
|
34
34
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
35
|
+
- `.claude/skills/morph-hotfix/SKILL.md` — fluxo completo passo a passo
|
|
36
|
+
- `.morph/framework/MORPH.md` §2.1 — roteamento por tipo de trabalho
|
|
37
|
+
- `.morph/framework/MORPH.md` §5 — Gates de aprovação, gate humano forçado no hotfix
|
|
38
38
|
|
|
39
39
|
**Feature:** $ARGUMENTS
|
|
@@ -20,7 +20,7 @@ Conduz a feature do request inicial até o Gate 2 (plano aprovado), sem interven
|
|
|
20
20
|
- Se existe e tem `0-proposal/proposal.md`: pular para passo 3.
|
|
21
21
|
- Se não existe: criar a estrutura e seguir o passo 2.
|
|
22
22
|
|
|
23
|
-
2. **Invocar `morph-brainstorming`** (skill em
|
|
23
|
+
2. **Invocar `morph-brainstorming`** (skill em `.claude/skills/morph-brainstorming/`):
|
|
24
24
|
- Varre o ambiente **antes** de qualquer pergunta (stack, contexto do projeto, acervo de use cases).
|
|
25
25
|
- Monta a árvore de decisões abertas — só o que a varredura não respondeu.
|
|
26
26
|
- Abre com a escolha de **modo de descoberta** (entrevista ou automático), recomendando um.
|
|
@@ -31,7 +31,7 @@ Conduz a feature do request inicial até o Gate 2 (plano aprovado), sem interven
|
|
|
31
31
|
- Resposta válida: "aprovado", "pode avançar", "lgtm".
|
|
32
32
|
- Se ajustes solicitados: aplicar e reapresentar o gate.
|
|
33
33
|
|
|
34
|
-
4. **Após Gate 1, invocar `morph-plan`** (skill em
|
|
34
|
+
4. **Após Gate 1, invocar `morph-plan`** (skill em `.claude/skills/morph-plan/`):
|
|
35
35
|
- Lê `proposal.md`.
|
|
36
36
|
- Produz `spec.md`, `mandate.md`, `tasks.json` em `.morph/features/{feature-name}/2-plan/`.
|
|
37
37
|
- Apresenta o **Gate 2** com template fixo (ver SKILL.md passo 5).
|
|
@@ -50,8 +50,8 @@ Conduz a feature do request inicial até o Gate 2 (plano aprovado), sem interven
|
|
|
50
50
|
|
|
51
51
|
## Referências
|
|
52
52
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
53
|
+
- `.morph/framework/MORPH.md` — charter completo
|
|
54
|
+
- `.claude/skills/morph-brainstorming/SKILL.md`
|
|
55
|
+
- `.claude/skills/morph-plan/SKILL.md`
|
|
56
56
|
|
|
57
57
|
**Feature:** $ARGUMENTS
|
package/framework/docs/CLI.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
|
|
10
10
|
- **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
|
|
11
11
|
- **`morph-spec score <feature> <task> <score> [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
|
|
12
|
-
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato)
|
|
12
|
+
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
13
13
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
14
14
|
|
|
15
15
|
## Features
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
## Worktree / Fleet
|
|
23
23
|
|
|
24
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
|
|
24
|
+
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra. Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
|
|
25
25
|
- **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
|
|
26
26
|
- **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
|
|
27
27
|
- **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
|
|
28
28
|
- **`morph-spec worktree remove [feature] [--target <path>] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho.
|
|
29
29
|
- **`morph-spec handoff <feature>`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
|
|
30
30
|
- **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
|
|
31
|
-
- **`morph-spec claim [feature] [--release] [--list] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`.
|
|
31
|
+
- **`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
|
|
32
32
|
|
|
33
33
|
## Validação
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
- **`morph-spec verify <feature> [task] [--json] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
|
|
37
37
|
- **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
|
|
38
38
|
- **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda o único reparo seguro (`git worktree prune` — registros de pastas que já sumiram); apagar pasta que ainda existe é reportado com o comando exato, nunca executado.
|
|
39
|
-
- **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez). Protocolo completo em MORPH.md §6.
|
|
39
|
+
- **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Protocolo completo em MORPH.md §6.
|
|
40
40
|
|
|
41
41
|
## Observabilidade
|
|
42
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rubrica Condensada — Eval Inline por Task
|
|
2
2
|
|
|
3
|
-
Versão compacta das 4 rubricas completas de
|
|
3
|
+
Versão compacta das 4 rubricas completas de `.morph/framework/evals/rubrics/`, para o eval
|
|
4
4
|
**inline por task** (`morph-eval` §2, escopo tarefa única). Mesmas dimensões, mesmos
|
|
5
5
|
pesos, mesma régua — só menos texto por banda.
|
|
6
6
|
|
|
@@ -46,4 +46,4 @@ Arredonde para uma casa decimal. ≥9 aprova em silêncio; <9 alimenta o loop de
|
|
|
46
46
|
## Quando abrir a completa em vez desta
|
|
47
47
|
|
|
48
48
|
Dúvida de banda, violação mecânica suspeita, ou escopo de fase (Gate 3) — abra
|
|
49
|
-
|
|
49
|
+
`.morph/framework/evals/rubrics/{dimension}.md`. Esta condensada é o atalho do caso comum.
|
|
@@ -51,3 +51,10 @@ Avalia se os testes cobrem os comportamentos esperados, são isolados e têm nom
|
|
|
51
51
|
3. O que acontece quando uma dependência falha — isso está testado?
|
|
52
52
|
4. Os testes podem rodar em qualquer ordem sem interferir entre si?
|
|
53
53
|
5. Os nomes dos testes descrevem o comportamento esperado sem precisar ler o corpo?
|
|
54
|
+
6. **Esta mudança é uma CORREÇÃO? Então: o que ela abriu?** Que comportamento antes correto pode ter
|
|
55
|
+
regredido por causa dela, e existe teste que pegaria essa regressão? Toda pergunta acima olha para
|
|
56
|
+
frente (o que a mudança deve fazer); esta olha para os lados. Numa feature real, três de três
|
|
57
|
+
rodadas de correção abriram um problema novo — o fix de um shadow abriu um laço de LLM a cada 15
|
|
58
|
+
min; o fecho de um fail-open abriu um P1; a correção desse P1 saiu com meio predicado. Nenhuma
|
|
59
|
+
apareceu no score, porque score não pergunta isso. **Sem resposta a esta pergunta numa correção,
|
|
60
|
+
o teto de Cobertura é 8.**
|
|
@@ -106,6 +106,93 @@ function diffPlanContract(oldDoc, newDoc, mutableFields) {
|
|
|
106
106
|
return violations;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* O heading que abre o bloco de errata, no fim de um arquivo congelado.
|
|
111
|
+
*
|
|
112
|
+
* Congelar o CONTRATO não é a mesma coisa que congelar um ERRO DE FATO. Caso
|
|
113
|
+
* real: a spec afirmava, entre parênteses, que certos registros também eram
|
|
114
|
+
* escritos por humanos; o código mostrava que os três escritores eram do
|
|
115
|
+
* próprio motor. A afirmação era falsa sobre o código, não uma mudança de
|
|
116
|
+
* escopo — mas a trava cega não distingue as duas coisas, então a mentira
|
|
117
|
+
* ficava na spec e a correção morava em outro arquivo. Quem abrisse a spec
|
|
118
|
+
* primeiro lia a versão errada.
|
|
119
|
+
*
|
|
120
|
+
* A errata resolve isso SEM abrir o contrato: o corpo continua byte-idêntico e
|
|
121
|
+
* só se acrescenta texto ao fim, num bloco identificado. Escopo negociado ⇒
|
|
122
|
+
* revogar o gate. Fato errado sobre o código ⇒ errata.
|
|
123
|
+
*/
|
|
124
|
+
const ERRATA_HEADING = '## Errata';
|
|
125
|
+
|
|
126
|
+
/** Índice onde começa o bloco de errata (-1 se não há). Só reconhece o heading em início de linha. */
|
|
127
|
+
function errataIndex(text) {
|
|
128
|
+
if (text.startsWith(ERRATA_HEADING)) return 0;
|
|
129
|
+
const at = text.indexOf(`\n${ERRATA_HEADING}`);
|
|
130
|
+
return at === -1 ? -1 : at + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Corpo congelado = tudo ANTES do bloco de errata (ou o texto inteiro, se não
|
|
135
|
+
* há bloco), sem o branco final.
|
|
136
|
+
*
|
|
137
|
+
* O `trimEnd` não é frouxidão: separar o bloco por uma linha em branco é o
|
|
138
|
+
* formato markdown natural, e sem isso a primeira errata de todas seria
|
|
139
|
+
* recusada por "mudou o corpo" — quando o que mudou foi um `\n` de formatação.
|
|
140
|
+
* A garantia que importa (nenhum caractere SIGNIFICATIVO do texto aprovado
|
|
141
|
+
* muda) fica intacta, porque só espaço em branco terminal é ignorado.
|
|
142
|
+
*/
|
|
143
|
+
function frozenBody(text) {
|
|
144
|
+
const at = errataIndex(text);
|
|
145
|
+
return (at === -1 ? text : text.slice(0, at)).trimEnd();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A mudança proposta é uma errata legítima?
|
|
150
|
+
*
|
|
151
|
+
* Três condições, todas necessárias — frouxo em qualquer uma delas e a trava
|
|
152
|
+
* cega vira uma trava que uma edição criativa atravessa:
|
|
153
|
+
* 1. o corpo congelado permanece BYTE-IDÊNTICO;
|
|
154
|
+
* 2. o resultado de fato tem um bloco `## Errata` (senão é edição do corpo);
|
|
155
|
+
* 3. a errata só CRESCE — o que já estava escrito nela não é reescrito.
|
|
156
|
+
*
|
|
157
|
+
* A (3) existe porque uma errata reescrita perde a serventia: o valor dela é
|
|
158
|
+
* ser trilha acumulada, não um segundo lugar editável.
|
|
159
|
+
*
|
|
160
|
+
* @returns {{ok: true} | {ok: false, why: string}}
|
|
161
|
+
*/
|
|
162
|
+
function errataViolation(original, proposed) {
|
|
163
|
+
if (frozenBody(proposed) !== frozenBody(original)) {
|
|
164
|
+
return { ok: false, why: 'o corpo da spec mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
|
|
165
|
+
}
|
|
166
|
+
if (errataIndex(proposed) === -1) {
|
|
167
|
+
return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
|
|
168
|
+
}
|
|
169
|
+
const oldAt = errataIndex(original);
|
|
170
|
+
if (oldAt !== -1) {
|
|
171
|
+
const oldErrata = original.slice(oldAt);
|
|
172
|
+
if (!proposed.slice(errataIndex(proposed)).startsWith(oldErrata)) {
|
|
173
|
+
return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
|
|
181
|
+
* @returns {string|null} null quando o payload não permite reconstruir (fail-open).
|
|
182
|
+
*/
|
|
183
|
+
function proposedText(payload, toolName, original) {
|
|
184
|
+
if (toolName === 'Edit') {
|
|
185
|
+
const oldStr = payload?.tool_input?.old_string;
|
|
186
|
+
const newStr = payload?.tool_input?.new_string;
|
|
187
|
+
if (typeof oldStr !== 'string' || typeof newStr !== 'string') return null;
|
|
188
|
+
return payload?.tool_input?.replace_all
|
|
189
|
+
? original.split(oldStr).join(newStr)
|
|
190
|
+
: original.replace(oldStr, newStr);
|
|
191
|
+
}
|
|
192
|
+
const content = payload?.tool_input?.content;
|
|
193
|
+
return typeof content === 'string' ? content : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
109
196
|
/**
|
|
110
197
|
* @param {object} payload - parsed PreToolUse stdin payload
|
|
111
198
|
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
@@ -219,14 +306,38 @@ export async function run(payload) {
|
|
|
219
306
|
);
|
|
220
307
|
}
|
|
221
308
|
|
|
222
|
-
//
|
|
309
|
+
// ── Arquivo totalmente travado (spec.md, mandate.md, design) ────────────
|
|
310
|
+
// O corpo continua congelado. A ÚNICA passagem é a errata append-only:
|
|
311
|
+
// corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
|
|
312
|
+
// de leitura/reconstrução cai no bloqueio (o conservador aqui é BLOQUEAR —
|
|
313
|
+
// ao contrário do ramo do tasks.json, onde não conseguir diffar significa
|
|
314
|
+
// não ter como provar violação; aqui a regra base é "não se edita").
|
|
315
|
+
const toolName = getToolName(payload);
|
|
316
|
+
const absPath = resolveTargetPath(filePath, root);
|
|
317
|
+
let errataCheck = null;
|
|
318
|
+
try {
|
|
319
|
+
const original = readFileSync(absPath, 'utf-8');
|
|
320
|
+
const proposed = proposedText(payload, toolName, original);
|
|
321
|
+
if (proposed !== null) errataCheck = errataViolation(original, proposed);
|
|
322
|
+
} catch {
|
|
323
|
+
// arquivo ilegível → sem errata possível, segue para o bloqueio
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (errataCheck?.ok) {
|
|
327
|
+
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
223
331
|
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
224
332
|
return blockResult(
|
|
225
333
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
226
334
|
`Editing approved specs breaks the spec contract.\n\n` +
|
|
227
|
-
|
|
228
|
-
`
|
|
229
|
-
`
|
|
335
|
+
(errataCheck ? `Esta mudança não é uma errata válida: ${errataCheck.why}.\n\n` : '') +
|
|
336
|
+
`Corrigir um FATO ERRADO sobre o código (a spec afirma algo que o código\n` +
|
|
337
|
+
`desmente) não exige revogar o gate: acrescente um bloco \`${ERRATA_HEADING}\` no\n` +
|
|
338
|
+
`FIM do arquivo, deixando o corpo intacto — é append-only e fica na trilha.\n\n` +
|
|
339
|
+
`Mudar o ESCOPO acordado é outra coisa: peça ao usuário para revogar o gate\n` +
|
|
340
|
+
`'${requiredGate}', edite, e re-apresente o gate para aprovação.`
|
|
230
341
|
);
|
|
231
342
|
} catch {
|
|
232
343
|
// Fail-open
|
|
@@ -160,10 +160,13 @@ export function getToolName(payload) {
|
|
|
160
160
|
return payload?.tool_name || 'unknown';
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Assinatura de Bash: comando normalizado, truncado. Ver getCommandSignature. */
|
|
164
|
+
const BASH_SIGNATURE_MAX = 120;
|
|
165
|
+
|
|
163
166
|
/**
|
|
164
167
|
* Build a normalized signature for loop detection.
|
|
165
168
|
* - Edit/Write: "Edit:normalized/file/path"
|
|
166
|
-
* - Bash: "Bash
|
|
169
|
+
* - Bash: "Bash:<normalized command, truncated>"
|
|
167
170
|
* - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
|
|
168
171
|
* - No tool_input at all: just the tool name
|
|
169
172
|
*
|
|
@@ -175,6 +178,17 @@ export function getToolName(payload) {
|
|
|
175
178
|
* false positive. Folding in the actual input (rather than hand-listing a
|
|
176
179
|
* target field per tool) fixes it uniformly for every tool at once.
|
|
177
180
|
*
|
|
181
|
+
* **Bash ficou de fora daquele fix (2026-07-22) e continuou colapsando em DUAS
|
|
182
|
+
* PALAVRAS**, o que é quase sempre o nome da ferramenta e nada mais:
|
|
183
|
+
* `npm test -- a`, `npm test -- b` e `npm run build` viravam `Bash:npm test` /
|
|
184
|
+
* `Bash:npm run`. Um ciclo legítimo de mutação — rodar o mesmo runner sobre
|
|
185
|
+
* alvos diferentes, que é exatamente como se testa se um teste morde — disparava
|
|
186
|
+
* o aviso de loop três vezes seguidas. Aviso que toca à toa é aviso que se
|
|
187
|
+
* aprende a ignorar, e aí ele não serve nem quando está certo.
|
|
188
|
+
*
|
|
189
|
+
* O discriminador agora é o comando inteiro (normalizado e truncado), mesma
|
|
190
|
+
* forma dos demais tools. Repetir o comando IDÊNTICO continua sendo loop.
|
|
191
|
+
*
|
|
178
192
|
* @param {Object} payload - Hook payload from stdin
|
|
179
193
|
* @returns {string} Signature string
|
|
180
194
|
*/
|
|
@@ -191,9 +205,10 @@ export function getCommandSignature(payload) {
|
|
|
191
205
|
}
|
|
192
206
|
|
|
193
207
|
if (tool === 'Bash') {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
// Espaços colapsados para que a mesma invocação escrita com espaçamento
|
|
209
|
+
// diferente continue casando — o alvo é o comando, não a digitação.
|
|
210
|
+
const cmd = (input.command || '').replace(/\s+/g, ' ').trim();
|
|
211
|
+
return cmd ? `Bash:${cmd.slice(0, BASH_SIGNATURE_MAX)}` : 'Bash';
|
|
197
212
|
}
|
|
198
213
|
|
|
199
214
|
try {
|
|
@@ -83,4 +83,5 @@ For each new feature slice, follow the patterns in these templates:
|
|
|
83
83
|
|
|
84
84
|
1. **Read the template** to see the exact pattern
|
|
85
85
|
2. **Generate code following the same structure** (naming, namespaces, imports)
|
|
86
|
-
3. Or
|
|
86
|
+
3. Or scaffold a whole VSA slice from the templates at once:
|
|
87
|
+
`morph-spec scaffold dotnet <feature> <slice-name>`
|