@polymorphism-tech/morph-spec 9.8.0 → 10.1.1
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/CLAUDE.md +12 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +28 -36
- package/docs/QUICKSTART.md +31 -191
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-preflight.md +3 -3
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +84 -95
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/mcp/registry.json +4 -4
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +10 -2
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +11 -18
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +39 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +49 -109
- package/framework/skills/core/morph-plan/references/critique.md +43 -0
- package/framework/skills/core/morph-plan/references/interview.md +37 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +51 -28
- package/framework/standards/STANDARDS.meta.json +16 -19
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +70 -0
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +15 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +3 -14
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +2 -2
- package/src/commands/state/create.js +1 -6
- package/src/commands/state/gate-check.js +38 -31
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/installers/memory-installer.js +3 -3
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +61 -109
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +28 -4
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
|
@@ -18,7 +18,7 @@ relatório começa igual e é append-only (vale o último `**Verdict:**`):
|
|
|
18
18
|
- (o que melhoraria, sem bloquear)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Sem bloqueador, escreva `Nenhum.` na seção. `SHIP` com bloqueador aberto é contradição e pausa o gate.
|
|
21
|
+
O `**Reviewed-SHA:**` é obrigatório: o `approve` recusa relatório sem ele ou de outro sha. Sem bloqueador, escreva `Nenhum.` na seção. `SHIP` com bloqueador aberto é contradição e pausa o gate.
|
|
22
22
|
Confirmação depois de uma correção: seção nova ao fim com `**Verdict:**` e o `**Reviewed-SHA:**` novo —
|
|
23
23
|
julgando só os seus bloqueadores, ou, sem eles, só o delta `{sha anterior}..{sha novo}`. Bloqueador
|
|
24
24
|
que a correção fechou sai de `## Bloqueadores` (escreva `Nenhum.`) e o fechamento vai em `## Notas`;
|
|
@@ -31,70 +31,58 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
|
|
|
31
31
|
| Tipo | Quem | Quando | Evidência mínima |
|
|
32
32
|
|------|------|--------|------------------|
|
|
33
33
|
| `check` | verificador | cenário `Cn` NÃO PROVADO ou FALHA | o que falta provar, ou o repro que falha |
|
|
34
|
-
| `teste` | verificador | teste vermelho; teste que não morde a regra que diz proteger | saída do
|
|
34
|
+
| `teste` | verificador | teste vermelho; teste que não morde a regra que diz proteger; Cn de risco sem teste | saída do teste filtrado, assert que passa pelo motivo errado |
|
|
35
35
|
| `regressao` | revisor | comportamento que funcionava e quebrou | consumidor afetado + o caminho que quebra |
|
|
36
|
-
| `escopo` | revisor | o diff faz o que
|
|
36
|
+
| `escopo` | revisor | o diff faz o que nenhum cenário pediu, ou deixa de fazer o que o contrato pede | trecho que nenhum Cn explica, ou o item do contrato ausente |
|
|
37
37
|
| `arquitetura` | revisor | padrão ou abstração nova contra o stack e os standards; item da lista fechada de `architecture-engineering-lens#review-blockers` que o DIFF introduziu (o que já existia é nota) | o trecho e o standard que ele contraria |
|
|
38
38
|
| `performance` | revisor | custo que o volume real torna visível | a consulta/lista e o volume que a derruba |
|
|
39
|
+
| `irreversivel` | revisor | o diff faz o que um revert não desfaz e nenhum `Cn` declara `degrau: migrar`/`irreversível`: apaga ou reescreve dado fora de migration, envia a terceiro, cobra, muda resposta de API ou webhook que alguém consome | o trecho e o efeito que fica depois do revert. Vai direto ao usuário, sem rodada de correção |
|
|
39
40
|
| `seguranca` | segurança | authz, segredo, injeção, PII, custo sem teto, migration destrutiva | o caminho de exploração |
|
|
40
41
|
|
|
41
42
|
## Verificador (`4-review/verifier.md`)
|
|
42
43
|
|
|
43
|
-
Missão: **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
Missão: **fazer cada cenário acontecer** e dizer se o resultado é o que o `2-plan/checks.json`
|
|
45
|
+
promete. Você recebe os cenários, a URL da app e o comando de reset — não o plano, o diff nem o relato
|
|
46
|
+
de quem implementou, de propósito: você prova o comportamento, não a versão dele que o implementador
|
|
47
|
+
conta. O default de todo `Cn` é NÃO PROVADO; o que você não executou (ou, em `leitura`, não leu)
|
|
48
|
+
nunca vira PROVADO por inferência.
|
|
48
49
|
|
|
49
50
|
```markdown
|
|
50
51
|
## Checks
|
|
51
52
|
|
|
52
53
|
| Cn | status | evidência |
|
|
53
54
|
|----|--------|-----------|
|
|
54
|
-
| C1 | PROVADO | `
|
|
55
|
-
| C2 |
|
|
55
|
+
| C1 | PROVADO | `POST /orders {items:[]}` → 422 "pedido sem itens"; `OrdersTests.RecusaSemItens` verde |
|
|
56
|
+
| C2 | FALHA | /tenants?status=ativo lista 3 inativos (print `4-review/c2.png`) |
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
Uma linha por cenário, status exato `PROVADO`, `NÃO PROVADO` ou `FALHA`. O gate pausa com cenário
|
|
59
|
-
sem linha ou não PROVADO. Numa confirmação, uma `## Checks` nova só com os Cn
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
execução que prova mordida é a mutação abaixo; o resto, por leitura.
|
|
76
|
-
|
|
77
|
-
**Prova por mutação — opcional, no máximo 3, só depois que revisor e segurança terminaram.** Só
|
|
78
|
-
quando o diff tem uma linha de guarda ou regra que um cenário exige e você não achou o teste que a
|
|
79
|
-
morde. Escreva `4-review/mutants.json` e rode `morph-spec mutate {feature}` **uma vez**:
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a",
|
|
83
|
-
"expect":"OrdersTests.RecusaSemItens deve falhar: a regra de itens some"}]}
|
|
84
|
-
```
|
|
60
|
+
sem linha ou não PROVADO. Numa confirmação, uma `## Checks` nova só com os Cn pedidos: vale a última
|
|
61
|
+
linha de cada Cn.
|
|
62
|
+
|
|
63
|
+
**Como provar, pelo `prova:` do Cn:**
|
|
64
|
+
|
|
65
|
+
| `prova:` | Execução |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `e2e` | Navegue o fluxo com o Playwright MCP na URL do dispatch, como o usuário faria; confira o resultado na tela, no console e no network. Rode `morph-spec e2e reset {feature}` **antes de cada fluxo**: estado de um fluxo nunca prova o seguinte. |
|
|
68
|
+
| `teste` | Ache o teste que cobre o Cn e rode **só ele**, pelo filtro do próprio runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`, `npx playwright test <arquivo>`). `verify --filter` escolhe um nó inteiro da suíte: só serve quando o nó é esse teste. Verde não basta: leia o teste — ele morde? Nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, ou o assert passa pelo motivo errado → bloqueador `teste`. Sem teste para um Cn com `risco:` → `teste`. |
|
|
69
|
+
| `eval` | Rode o nó de evals filtrado (`--filter <nó-evals> --skip-e2e --no-persist`) e compare a saída com o cenário. |
|
|
70
|
+
| `leitura` | `arquivo:linha` do código que entrega o cenário e a **fiação**: caminho da entrada real ao efeito (a camada pura não prova que o handler a chama). Sem teste, e pedir teste não é bloqueador. |
|
|
71
|
+
| `manual` | Reproduza o passo de `3-implement/evidencias/{Cn}.md` e diga se o resultado bate. |
|
|
72
|
+
|
|
73
|
+
Verificações mecânicas valem para todo Cn de backend (.NET minimal API / Next): serviço injetado numa
|
|
74
|
+
lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como
|
|
75
|
+
constantes distintas.
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Sem subconjunto (exit 2) ou `inconclusive`: a mutação não roda a suíte inteira — prove por leitura
|
|
90
|
-
(`arquivo:linha` + o teste que cobre), nunca em laço. Cole a
|
|
91
|
-
seção `## Mutações` que a ferramenta imprime. A reversão é da ferramenta; rodada morta no meio:
|
|
92
|
-
`morph-spec mutate --restore`.
|
|
77
|
+
Você não roda a suíte inteira nem `verify` sem filtro (o hook `test-budget` nega): ela já rodou antes
|
|
78
|
+
do dispatch. Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — achado de
|
|
79
|
+
infra, não defeito seu a provar; não suba nem derrube o stack.
|
|
93
80
|
|
|
94
|
-
**Feature que gera mídia** (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
**Feature que gera mídia** (o `plan.md` cita um standard de mídia): grave `## Mídia gerada`, uma
|
|
82
|
+
linha por afirmação — custo controlado, revisão humana, mídia marcada, hot path não bloqueado —, cada
|
|
83
|
+
uma com o código que a prende e o teste que a prova. A régua de cada afirmação está em
|
|
84
|
+
`.morph/framework/standards/ai-agents/modalities-image-gen.md`, `ai-agents/media-video.md` e
|
|
85
|
+
`ai-agents/media-pipeline.md`. Afirmação falsa é bloqueador. Sem a seção, o gate pausa.
|
|
98
86
|
|
|
99
87
|
**Prompt ou modelo alterado**: o nó `evals` do verify é a prova; sem ele, o gate pausa sozinho.
|
|
100
88
|
|
|
@@ -110,23 +98,27 @@ havendo item decidido, o gate pausa.
|
|
|
110
98
|
|
|
111
99
|
## Revisor (`4-review/reviewer.md`)
|
|
112
100
|
|
|
113
|
-
Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}:<caminho
|
|
114
|
-
árvore de trabalho
|
|
101
|
+
Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, e o
|
|
102
|
+
`2-plan/plan.md` (a spec). Nunca a árvore de trabalho: o commit é o que foi provado. Default SHIP.
|
|
115
103
|
|
|
116
|
-
1. **Escopo**: o diff contra
|
|
104
|
+
1. **Escopo**: o diff contra os cenários e o `## Contrato de implementação` do plano.
|
|
117
105
|
Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
|
|
118
106
|
risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
|
|
119
|
-
camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê
|
|
120
|
-
|
|
107
|
+
camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê nas `## Notas` do
|
|
108
|
+
`report.md`, edição de `.sln`/`.csproj`/glob do runner que tira testes da execução, ou `expectedCount` menor no `.morph/config/config.json` do diff sem teste apagado no mesmo
|
|
121
109
|
diff, é bloqueador `regressao`: é suíte que parou de rodar, não limpeza.
|
|
122
110
|
2. **Arquitetura**: padrão ou abstração nova contra o stack do projeto e os standards (camada que o
|
|
123
111
|
projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano).
|
|
124
|
-
3. **Regressão**:
|
|
112
|
+
3. **Regressão**: abra pela árvore do que mudou, `morph-spec graph diff --base {base} --json` (símbolos
|
|
113
|
+
tocados `+`/`~` e quem chama quem). Se ela recusar (`stale-graph`, `graph-drift`, `no-graph`), leia o
|
|
114
|
+
diff cru. Para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`.
|
|
125
115
|
Sem grafo (`no-graph`, `not-found`, `ambiguous`), caia para `git grep -n <símbolo> {sha}` e siga — a
|
|
126
116
|
ausência de grafo nunca bloqueia, e ausência de consumidor nunca sai do grafo.
|
|
127
117
|
4. **O que a correção abriu**: revisando a correção de uma rodada anterior, releia o comportamento
|
|
128
118
|
vizinho ao que ela mudou.
|
|
129
119
|
5. **Performance só com evidência**: lista sem paginação no servidor sobre volume real, N+1 visível.
|
|
120
|
+
**Irreversível não declarado**: o que fica depois de um `git revert` (dado apagado ou reescrito, mensagem
|
|
121
|
+
enviada, cobrança, contrato externo mudado) sem um `Cn` com `degrau:` que o cubra → bloqueador `irreversivel`.
|
|
130
122
|
6. **Chamada a LLM**: dá para reconstruir o que foi ao provedor e o que ele respondeu neste turno?
|
|
131
123
|
Não → nota.
|
|
132
124
|
7. **Tela no diff**: cor ou espaçamento literal fora do CSS de tokens, mais de uma ação primária,
|
|
@@ -7,9 +7,9 @@ Comprehensive hooks system for enforcing spec-driven development at the Claude C
|
|
|
7
7
|
```
|
|
8
8
|
framework/hooks/
|
|
9
9
|
├── claude-code/
|
|
10
|
-
│
|
|
11
|
-
│ └── statusline.py # global statusline
|
|
10
|
+
│ └── core/ # every hook, plus chain.js (one process per multi-hook group)
|
|
12
11
|
├── shared/ # code hooks share (and src/ imports): state reader, guards, e2e, heavy, telemetry
|
|
12
|
+
├── statusline.js # global statusline, not a hook: phase and scenarios through the project's .morph/framework/hooks/shared
|
|
13
13
|
└── README.md
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -28,8 +28,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
28
28
|
| **PreToolUse** (Bash) | conventional-commit.js | Block | Blocks a `git commit -m` whose subject is not Conventional Commits (`<type>(<scope>): <subject>`); a `$(…)`/heredoc message, `-F` and git-generated `Merge`/`Revert` pass |
|
|
29
29
|
| **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
|
|
30
30
|
| **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
|
|
31
|
-
| **
|
|
32
|
-
| **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), bases a task tree on the dispatcher's `morph/<f>` branch when `worktree.baseRef` is unset (`"head"` = current HEAD, `"fresh"` = default branch), prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
|
|
31
|
+
| **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), bases a task tree on the default branch, prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
|
|
33
32
|
| **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
|
|
34
33
|
| **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (ADR, scenario/slice id, gate, plan.md), banner/`#region`, a block over 3 lines, or a doc-comment outside a published package. Old comments are never reported; last link of the chain |
|
|
35
34
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
@@ -37,6 +36,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
37
36
|
| **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec telemetry` |
|
|
38
37
|
| **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
|
|
39
38
|
| **PostToolUse** (all tools) | graph-refresh-kick.js | Background | After a Bash or PowerShell command that runs git/gh/morph-spec, launches the DETACHED refresh of the ROOT's codebase graph (`shared/graph-refresh.js`: cwd in the git common dir, never a worktree; one lock per repository; staleness by `shared/graph-freshness.js` — stamp plus the per-tree run record in `<git-common-dir>/morph-spec/graph-refresh-state.json`); never waits, never injects |
|
|
39
|
+
| **PostToolUse** (all tools) | masked-exit.js | Advisory | After a Bash test/build/verify run (`testRunKind`/`morphVerifyCall`/`heavyKind` of the producer) whose exit `exitCodeMasked` hides (`\| tail`, `; echo`, `\|\| true`, `&`), names the form and says to write the output to `.morph/logs/test-last.log` and read it; silent when the command prints `$?`/`PIPESTATUS` |
|
|
40
40
|
| **PostToolUseFailure** (all tools) | telemetry-log.js | Telemetry | The failed call (a Bash that exits non-zero fires only this event): exit code parsed from the `Exit code N` line, outcome and duration — never the error body |
|
|
41
41
|
| **PostToolBatch** | telemetry-log.js | Telemetry | One `ToolBatch` event per model turn that used tools (the turn counter of `cost --metrics`) |
|
|
42
42
|
| **SubagentStop** | telemetry-log.js | Telemetry | `AgentUsage`: the sub-agent's token usage read from its own transcript, as a delta per agent; never blocks the stop |
|
|
@@ -4,4 +4,4 @@ See `framework/MORPH.md` for full documentation.
|
|
|
4
4
|
|
|
5
5
|
## test-budget
|
|
6
6
|
|
|
7
|
-
`PreToolUse` Bash (antes do `heavy-gate`)
|
|
7
|
+
`PreToolUse` Bash (antes do `heavy-gate`). Só age nos agentes do Gate 3 (`agent_id` com `agent_type` em `REVIEW_AGENT_TYPES`: `morph-verifier`/`morph-reviewer`/`morph-security`) com `.morph/` presente. Nega `morph-spec verify` sem `--filter` e a suíte de teste inteira; libera o teste filtrado (`testRunCall` de `framework/hooks/shared/heavy-command.js` decide o que é filtro). A sessão principal e os demais sub-agents não são tocados. Sem contador. Fail-open.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.4.0",
|
|
3
3
|
"patterns": [
|
|
4
4
|
"DROP TABLE",
|
|
5
5
|
"DROP DATABASE",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
],
|
|
77
77
|
"subagentOnly": true,
|
|
78
78
|
"display": "git add -A",
|
|
79
|
-
"reason": "
|
|
79
|
+
"reason": "altera a árvore que o Gate 3 está lendo"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"executable": "git",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
],
|
|
86
86
|
"subagentOnly": true,
|
|
87
87
|
"display": "git stash",
|
|
88
|
-
"reason": "
|
|
88
|
+
"reason": "altera a árvore que o Gate 3 está lendo"
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
"executable": "git",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
],
|
|
101
101
|
"subagentOnly": true,
|
|
102
102
|
"display": "git checkout --",
|
|
103
|
-
"reason": "
|
|
103
|
+
"reason": "altera a árvore que o Gate 3 está lendo"
|
|
104
104
|
}
|
|
105
105
|
]
|
|
106
106
|
}
|
|
@@ -70,6 +70,15 @@ function checkAskUserQuestion(input, sessionRoot) {
|
|
|
70
70
|
'PostToolUse'
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
|
+
if (decision.resolver === 'session') {
|
|
74
|
+
return contextResult(
|
|
75
|
+
`⚠️ [morph-spec] GATE ASKED TOO EARLY — o Gate "${gate}" pausou com resolver:"session" ` +
|
|
76
|
+
`(${decision.reason || 'sinal mecânico'}): essa pausa não é do humano. ` +
|
|
77
|
+
`Faça o que a razão diz e rode \`morph-spec gate-check ${feature} ${gate}\` de novo; ` +
|
|
78
|
+
`a pergunta só vem quando o gate-check devolver resolver:"human".`,
|
|
79
|
+
'PostToolUse'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
73
82
|
return null;
|
|
74
83
|
}
|
|
75
84
|
return null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
4
|
+
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
5
|
+
import { exitCodeMasked, hasUnquotedPipe, producerOf } from '../../shared/command-tokens.js';
|
|
6
|
+
import { heavyKind, morphVerifyCall, testRunKind } from '../../shared/heavy-command.js';
|
|
7
|
+
|
|
8
|
+
// Printing `$?` or PIPESTATUS puts the real code in the output, so the session already sees it.
|
|
9
|
+
const EXIT_SHOWN = /\$\?|PIPESTATUS/;
|
|
10
|
+
|
|
11
|
+
function runKind(producer) {
|
|
12
|
+
if (morphVerifyCall(producer)) return 'morph-spec verify';
|
|
13
|
+
return testRunKind(producer) ?? heavyKind(producer);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function maskForm(command) {
|
|
17
|
+
const bare = command.replace(/"[^"]*"|'[^']*'/g, '""');
|
|
18
|
+
if (hasUnquotedPipe(command) && !/\bpipefail\b/.test(command)) {
|
|
19
|
+
const filter = /(?<![|>])\|(?!\|)\s*([^\s|;&]+)/.exec(bare)?.[1];
|
|
20
|
+
return filter ? `| ${filter}` : '|';
|
|
21
|
+
}
|
|
22
|
+
const or = /\|\|\s*([^\s;&|]+)/.exec(bare);
|
|
23
|
+
if (or) return `|| ${or[1]}`;
|
|
24
|
+
const reporter = /;\s*(echo|printf|true|:|exit\s+0)\b/.exec(bare);
|
|
25
|
+
if (reporter) return `; ${reporter[1]}`;
|
|
26
|
+
if (/(?<![&>])&(?![&>])/.test(bare)) return '&';
|
|
27
|
+
return 'o comando que vem depois';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function run(payload) {
|
|
31
|
+
try {
|
|
32
|
+
if (payload?.tool_name !== 'Bash') return null;
|
|
33
|
+
const command = payload.tool_input?.command;
|
|
34
|
+
if (typeof command !== 'string' || !command.trim()) return null;
|
|
35
|
+
if (!stateExists(payload.cwd || undefined)) return null;
|
|
36
|
+
if (!exitCodeMasked(command) || EXIT_SHOWN.test(command)) return null;
|
|
37
|
+
const kind = runKind(producerOf(command));
|
|
38
|
+
if (!kind) return null;
|
|
39
|
+
return contextResult(
|
|
40
|
+
`⚠️ [morph-spec] O exit de \`${kind}\` está mascarado por \`${maskForm(command)}\`: o código que você viu é o do último comando, não o do teste/build. `
|
|
41
|
+
+ 'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`) e leia o arquivo.',
|
|
42
|
+
'PostToolUse'
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await runStandalone(run);
|
|
@@ -164,8 +164,11 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
|
|
167
|
+
// The reports live in the tree that ran the review, which may not be the one the feature was found in.
|
|
168
|
+
const trees = allWorkTrees(sessionRoot);
|
|
169
|
+
const reportIn = (f) => trees.some((t) => existsSync(join(t, '.morph', 'features', featureName, '4-review', f)));
|
|
167
170
|
const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
|
|
168
|
-
&& (feature?.workType === 'chore' ? ['reviewer.md'] : ['verifier.md', 'reviewer.md']).some((f) => !
|
|
171
|
+
&& (feature?.workType === 'chore' ? ['reviewer.md'] : ['verifier.md', 'reviewer.md']).some((f) => !reportIn(f));
|
|
169
172
|
if (selfAssessed) {
|
|
170
173
|
warnings.push(
|
|
171
174
|
`⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
|
|
@@ -46,8 +46,7 @@ function classifyReadKind(target) {
|
|
|
46
46
|
if (inFeature) {
|
|
47
47
|
const rel = inFeature[1];
|
|
48
48
|
const PLAN_ARTIFACT_BASENAMES = new Set([
|
|
49
|
-
'plan.md', 'checks.json',
|
|
50
|
-
'handoff.md',
|
|
49
|
+
'plan.md', 'checks.json', 'report.md',
|
|
51
50
|
]);
|
|
52
51
|
const base = rel.split('/').pop();
|
|
53
52
|
if (PLAN_ARTIFACT_BASENAMES.has(base) || rel.includes('reports/')) {
|
|
@@ -1,87 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
2
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
5
3
|
import { stateExists } from '../../shared/state-reader.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
export const TEST_RUN_BUDGET = 3;
|
|
4
|
+
import { REVIEW_AGENT_TYPES, morphVerifyCall, testRunCall } from '../../shared/heavy-command.js';
|
|
9
5
|
|
|
10
6
|
export const REASONS = {
|
|
11
|
-
verify: '`morph-spec verify`
|
|
12
|
-
|
|
13
|
-
review: 'Agente de review não roda teste: a prova é o verify do orquestrador. Para provar que um teste morde, `morph-spec mutate`.',
|
|
7
|
+
verify: '`morph-spec verify` sem `--filter` é a prova do gate, rodada pela sessão. Rode só o nó do cenário: `morph-spec verify <f> --filter <nó> --no-persist`.',
|
|
8
|
+
suite: 'Suíte inteira não: rode só o teste do cenário que você está provando (o arquivo, `-t <nome>` ou `--filter <expr>`).',
|
|
14
9
|
};
|
|
15
10
|
|
|
16
|
-
// Pre and Post must land on the same counter: a sub-agent's cwd drifts into subfolders.
|
|
17
11
|
const projectRoot = (payload, env) => env.CLAUDE_PROJECT_DIR || payload?.cwd || process.cwd();
|
|
18
12
|
|
|
19
|
-
const counterPath = (root, agentId) =>
|
|
20
|
-
join(root, '.morph', 'memory', 'test-runs', `${String(agentId).replace(/[^A-Za-z0-9_-]/g, '_')}.json`);
|
|
21
|
-
|
|
22
|
-
function readCounter(path) {
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(readFileSync(path, 'utf-8')) ?? {};
|
|
25
|
-
} catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const readRuns = (path) => Number(readCounter(path).runs) || 0;
|
|
31
|
-
|
|
32
|
-
function recordRun(path, name) {
|
|
33
|
-
mkdirSync(join(path, '..'), { recursive: true });
|
|
34
|
-
const tmp = `${path}.tmp.${process.pid}`;
|
|
35
|
-
writeFileSync(tmp, JSON.stringify({ runs: readRuns(path) + 1, ...(name ? { name } : {}) }));
|
|
36
|
-
renameSync(tmp, path);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function exitCodeOf(payload) {
|
|
40
|
-
const code = /^exit code (\d+)/i.exec(String(payload.error ?? '').split('\n', 1)[0]);
|
|
41
|
-
return code ? Number(code[1]) : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// The orchestrator resuming an agent is the decision the budget hands back to it: a fresh budget.
|
|
45
|
-
function resetOnResume(payload, env) {
|
|
46
|
-
const to = payload.tool_input?.to;
|
|
47
|
-
if (payload.agent_id || payload.hook_event_name !== 'PostToolUse' || !to) return;
|
|
48
|
-
const root = projectRoot(payload, env);
|
|
49
|
-
if (!stateExists(root)) return;
|
|
50
|
-
// `to` may be the dispatch name instead of the id: the counter records the name it ran under.
|
|
51
|
-
const byId = counterPath(root, to);
|
|
52
|
-
const dir = join(byId, '..');
|
|
53
|
-
for (const file of readdirSync(dir)) {
|
|
54
|
-
const path = join(dir, file);
|
|
55
|
-
if (path === byId || readCounter(path).name === to) rmSync(path, { force: true });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
13
|
export async function run(payload, deps = {}) {
|
|
60
14
|
try {
|
|
61
|
-
if (payload?.tool_name
|
|
62
|
-
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (payload?.tool_name !== 'Bash' || !payload.agent_id) return null;
|
|
15
|
+
if (payload?.hook_event_name !== 'PreToolUse' || payload.tool_name !== 'Bash') return null;
|
|
16
|
+
if (!payload.agent_id || !REVIEW_AGENT_TYPES.has(payload.agent_type)) return null;
|
|
66
17
|
const command = payload.tool_input?.command || '';
|
|
67
|
-
const kind = testRunKind(command);
|
|
68
18
|
const verify = morphVerifyCall(command);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const root = projectRoot(payload, env);
|
|
73
|
-
if (!stateExists(root)) return null;
|
|
74
|
-
const path = counterPath(root, payload.agent_id);
|
|
75
|
-
|
|
76
|
-
if (payload.hook_event_name === 'PreToolUse') {
|
|
77
|
-
if (REVIEW_AGENT_TYPES.has(payload.agent_type)) return blockResult(REASONS.review);
|
|
78
|
-
if (verify && !verify.filtered) return blockResult(REASONS.verify);
|
|
79
|
-
if (readRuns(path) >= TEST_RUN_BUDGET) return blockResult(REASONS.budget);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
19
|
+
const tests = testRunCall(command);
|
|
20
|
+
if (!verify && !tests) return null;
|
|
21
|
+
if (!stateExists(projectRoot(payload, deps.env ?? process.env))) return null;
|
|
82
22
|
|
|
83
|
-
if (
|
|
84
|
-
if (
|
|
23
|
+
if (verify && !verify.filtered) return blockResult(REASONS.verify);
|
|
24
|
+
if (tests && !tests.filtered) return blockResult(REASONS.suite);
|
|
85
25
|
} catch {}
|
|
86
26
|
return null;
|
|
87
27
|
}
|
|
@@ -9,30 +9,17 @@ import {
|
|
|
9
9
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
10
10
|
import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
11
11
|
import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const { missing } = inspectReport(sliceReportPath(featurePath, slice.id), { covers: slice.covers });
|
|
22
|
-
if (missing.length) {
|
|
23
|
-
const hints = missing.map((id) => MISSING_HINT[id] ?? `cite ${id} pelo id`);
|
|
24
|
-
warnings.push(`relatório de ${slice.id} sem: ${missing.join(', ')} — ${hints.join('; ')} (formato na skill morph-apply; morph-spec report-check)`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return warnings;
|
|
12
|
+
import { implementProgress } from '../../shared/report-progress.js';
|
|
13
|
+
|
|
14
|
+
function auditReport(featurePath) {
|
|
15
|
+
const progress = implementProgress(featurePath);
|
|
16
|
+
if (!progress || progress.legacy) return [];
|
|
17
|
+
const bare = progress.checks.filter((c) => c.status === 'feito' && !c.evidence.trim()).map((c) => c.id);
|
|
18
|
+
return bare.length
|
|
19
|
+
? [`report.md: ${bare.join(', ')} feito sem evidência — o teste, a linha de código ou o passo que prova (morph-spec report-check)`]
|
|
20
|
+
: [];
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
const MISSING_HINT = {
|
|
31
|
-
what: '2-3 frases do que foi feito e como foi provado',
|
|
32
|
-
files: 'linha **Arquivos:** com todo arquivo criado ou modificado',
|
|
33
|
-
partial: '1ª linha PARCIAL: a fatia não terminou',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
23
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
37
24
|
|
|
38
25
|
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
@@ -89,7 +76,7 @@ export async function run(payload) {
|
|
|
89
76
|
// Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
|
|
90
77
|
try {
|
|
91
78
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
92
|
-
const reportWarnings =
|
|
79
|
+
const reportWarnings = auditReport(featurePath);
|
|
93
80
|
|
|
94
81
|
const warnedHashesPath = join(memDir, 'warned-hashes.json');
|
|
95
82
|
let warnedHashes = {};
|
|
@@ -100,7 +87,7 @@ export async function run(payload) {
|
|
|
100
87
|
if (reportWarnings.length > 0) {
|
|
101
88
|
const hash = createHash('sha1').update(reportWarnings.join('\n')).digest('hex');
|
|
102
89
|
if (warnedHashes[name]?.reports !== hash) {
|
|
103
|
-
warnings.push('📋
|
|
90
|
+
warnings.push('📋 3-implement/report.md:');
|
|
104
91
|
for (const w of reportWarnings) warnings.push(` • ${w}`);
|
|
105
92
|
warnedHashes[name] = { ...warnedHashes[name], reports: hash };
|
|
106
93
|
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {}
|
|
@@ -9,13 +9,12 @@ import {
|
|
|
9
9
|
getFeaturePhase,
|
|
10
10
|
getPendingGates,
|
|
11
11
|
} from '../../shared/state-reader.js';
|
|
12
|
-
import {
|
|
12
|
+
import { implementProgress } from '../../shared/report-progress.js';
|
|
13
13
|
import { collectNextSignals, decideNext, formatNext } from '../../shared/next-step.js';
|
|
14
14
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
15
15
|
import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
|
|
16
16
|
import { rotateEventsLogIfLarge } from '../../shared/telemetry-logger.js';
|
|
17
17
|
import { kickGraphRefresh, seedFromPrimary } from '../../shared/graph-refresh.js';
|
|
18
|
-
import { kickAgentReap } from '../../shared/agent-reap.js';
|
|
19
18
|
import { isInContainer } from '../../shared/worktree-root.js';
|
|
20
19
|
import { isSecondaryWorktree, resolveGitCommonDir } from '../../shared/git-paths.js';
|
|
21
20
|
|
|
@@ -121,18 +120,6 @@ function buildEnvLine(dir) {
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
function buildHandoffLine(dir, feature) {
|
|
125
|
-
try {
|
|
126
|
-
const handoffPath = join(dir, '.morph', 'features', feature, 'handoff.md');
|
|
127
|
-
if (!existsSync(handoffPath)) return null;
|
|
128
|
-
const generatedAt = readFileSync(handoffPath, 'utf8')
|
|
129
|
-
.match(/gerado em (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/)?.[1];
|
|
130
|
-
return `📄 Handoff: .morph/features/${feature}/handoff.md${generatedAt ? ` (gerado ${generatedAt})` : ''} — leia antes de continuar.`;
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
123
|
// No git here, so verify staleness is unknown: an escalation the tree already moved past, or the
|
|
137
124
|
// root skeleton of a feature living in a worktree, would be an order read from stale facts.
|
|
138
125
|
function buildNextLine(dir, feature) {
|
|
@@ -161,9 +148,6 @@ function buildWorktreeContext(dir, feature) {
|
|
|
161
148
|
const envLine = buildEnvLine(dir);
|
|
162
149
|
if (envLine) text += `\n${envLine}`;
|
|
163
150
|
|
|
164
|
-
const handoffLine = buildHandoffLine(dir, feature);
|
|
165
|
-
if (handoffLine) text += `\n${handoffLine}`;
|
|
166
|
-
|
|
167
151
|
text += buildMissingInfraWarning(dir);
|
|
168
152
|
return text;
|
|
169
153
|
}
|
|
@@ -185,11 +169,11 @@ function buildActiveFeatureSummary(dir) {
|
|
|
185
169
|
if (!active) return null;
|
|
186
170
|
|
|
187
171
|
const phase = getFeaturePhase(active.name, dir);
|
|
188
|
-
const
|
|
172
|
+
const progress = implementProgress(join(dir, '.morph', 'features', active.name));
|
|
189
173
|
const pending = getPendingGates(active.name, dir);
|
|
190
174
|
|
|
191
175
|
let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase})`;
|
|
192
|
-
if (
|
|
176
|
+
if (progress) text += ` · cenários ${progress.done}/${progress.total}`;
|
|
193
177
|
if (pending.length) {
|
|
194
178
|
text += ` · gates pendentes: ${pending.join(', ')}`;
|
|
195
179
|
}
|
|
@@ -207,8 +191,8 @@ function claimBlock(dir, feature, sessionId) {
|
|
|
207
191
|
if (!other) return '';
|
|
208
192
|
return `\n⚠️ ATENÇÃO — a feature ${feature} tem claim VIVO de OUTRA sessão`
|
|
209
193
|
+ `\n pid ${other.pid}${other.path ? ` · ${other.path}` : ''} · visto há ${claimAge(other)}`
|
|
210
|
-
+ '\n Duas sessões na mesma feature
|
|
211
|
-
+ '\n
|
|
194
|
+
+ '\n Duas sessões na mesma feature sobrescrevem o trabalho uma da outra.'
|
|
195
|
+
+ '\n Confirme com a outra sessão ANTES de agir —'
|
|
212
196
|
+ '\n principalmente antes de `finish`, `archive` ou `delete`.'
|
|
213
197
|
+ `\n Se aquela sessão morreu: morph-spec claim ${feature} --release`;
|
|
214
198
|
} catch {
|
|
@@ -229,7 +213,6 @@ export async function run(payload) {
|
|
|
229
213
|
// Only the ROOT's graph is refreshed in background: a detached process holding a worktree directory made `finish` fail on win32.
|
|
230
214
|
try { seedFromPrimary(dir); } catch {}
|
|
231
215
|
try { kickGraphRefresh(dir); } catch {}
|
|
232
|
-
try { kickAgentReap(dir); } catch {}
|
|
233
216
|
|
|
234
217
|
if (isSecondaryWorktree(dir)) {
|
|
235
218
|
const task = taskFromBranch(dir);
|