@polymorphism-tech/morph-spec 10.1.1 → 10.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/bin/morph-spec.js +10 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +1 -2
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +3 -4
- package/framework/docs/CLI.md +45 -22
- package/framework/evals/review-checklist.md +5 -5
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/heavy-gate.js +21 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/heavy-config.js +12 -0
- package/framework/hooks/shared/host-services.js +9 -6
- package/framework/hooks/shared/next-step.js +12 -8
- package/framework/phases.json +4 -4
- package/framework/rules/plan-grammar.md +4 -1
- package/framework/schemas/config.schema.json +59 -1
- package/framework/schemas/feature.schema.json +15 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +8 -5
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/machine.md +3 -2
- package/framework/skills/core/morph-apply/references/review.md +9 -7
- package/framework/skills/core/morph-plan/SKILL.md +7 -6
- package/framework/skills/core/morph-plan/references/critique.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +2 -1
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/project/heavy.js +13 -0
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +57 -7
- package/src/core/state/state-manager.js +0 -24
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/heavy/gate.js +24 -1
- package/src/lib/heavy/reclaim.js +107 -0
- package/src/lib/heavy/watchdog.js +46 -0
- package/src/lib/next-step.js +1 -0
- package/src/lib/plan/checks.js +1 -1
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/procs/prune.js +23 -1
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +10 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +131 -26
- package/src/lib/verify/affected.js +141 -0
- package/src/lib/verify/detached.js +154 -0
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 10.
|
|
6
|
+
**Version:** 10.5.2
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -115,7 +115,7 @@ In `trust: "auto"` mode (default), gates auto-pass without risk signals. See `fr
|
|
|
115
115
|
Check gate status:
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
morph-spec
|
|
118
|
+
morph-spec status {feature}
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
---
|
|
@@ -165,7 +165,6 @@ morph-spec fleet # Every active feature (root + wor
|
|
|
165
165
|
morph-spec status {feature} # Feature status dashboard
|
|
166
166
|
morph-spec approve {feature} {gate} # Approve a phase gate (plan, uiux, review)
|
|
167
167
|
morph-spec advance {feature} # Approve the current gate and scaffold the next phase folder
|
|
168
|
-
morph-spec approval-status {feature} # Show approval status for all gates
|
|
169
168
|
morph-spec finish {feature} --pr|--merge # Close a feature: archive + integrate (PR or local merge) + teardown
|
|
170
169
|
morph-spec archive {feature} # Archive WITHOUT integrating (legacy feature / cleanup)
|
|
171
170
|
```
|
|
@@ -177,9 +176,6 @@ Progress within a feature is the slice reports `3-implement/reports/F{n}.md`: ab
|
|
|
177
176
|
### Validation
|
|
178
177
|
|
|
179
178
|
```bash
|
|
180
|
-
morph-spec validate # Run all validators
|
|
181
|
-
morph-spec validate {validator} # Run specific validator
|
|
182
|
-
morph-spec validate-feature {feature} # Content-aware feature validation
|
|
183
179
|
morph-spec verify {feature} # Deterministic build+tests+validators chain, before the LLM judge
|
|
184
180
|
```
|
|
185
181
|
|
|
@@ -380,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
380
376
|
|
|
381
377
|
---
|
|
382
378
|
|
|
383
|
-
*morph-spec v10.
|
|
379
|
+
*morph-spec v10.5.2 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -18,6 +18,7 @@ const doctorCommand = lazy('../src/commands/project/doctor.js', 'doctorCommand')
|
|
|
18
18
|
const statusCommand = lazy('../src/commands/project/status.js', 'statusCommand');
|
|
19
19
|
const heavyStatusCommand = lazy('../src/commands/project/heavy.js', 'heavyStatusCommand');
|
|
20
20
|
const heavyRunCommand = lazy('../src/commands/project/heavy.js', 'heavyRunCommand');
|
|
21
|
+
const heavyReclaimCommand = lazy('../src/commands/project/heavy.js', 'heavyReclaimCommand');
|
|
21
22
|
const worktreeSetupCommand = lazy('../src/commands/project/worktree.js', 'worktreeSetupCommand');
|
|
22
23
|
const worktreeProvisionCommand = lazy('../src/commands/project/worktree.js', 'worktreeProvisionCommand');
|
|
23
24
|
const worktreeLinkCommand = lazy('../src/commands/project/worktree.js', 'worktreeLinkCommand');
|
|
@@ -35,7 +36,6 @@ const stackSetCommand = lazy('../src/commands/project/stack-set.js', 'stackSetCo
|
|
|
35
36
|
const testsAuditCommand = lazy('../src/commands/validation/tests-audit.js', 'testsAuditCommand');
|
|
36
37
|
|
|
37
38
|
const approveCommand = lazy('../src/commands/state/approve.js', 'approveCommand');
|
|
38
|
-
const approvalStatusCommand = lazy('../src/commands/state/approve.js', 'approvalStatusCommand');
|
|
39
39
|
const unapproveCommand = lazy('../src/commands/state/approve.js', 'unapproveCommand');
|
|
40
40
|
const advanceCommand = lazy('../src/commands/state/advance.js', 'advanceCommand');
|
|
41
41
|
const archiveCommand = lazy('../src/commands/state/archive.js', 'archiveCommand');
|
|
@@ -55,8 +55,6 @@ const graphAffectedCommand = lazy('../src/commands/graph/affected.js', 'graphAff
|
|
|
55
55
|
const graphExplainCommand = lazy('../src/commands/graph/explain.js', 'graphExplainCommand');
|
|
56
56
|
const graphDiffCommand = lazy('../src/commands/graph/diff.js', 'graphDiffCommand');
|
|
57
57
|
|
|
58
|
-
const validateCommand = lazy('./validate.js', 'validateCommand');
|
|
59
|
-
const validateFeatureCommand = lazy('../src/commands/validation/validate-feature.js', 'validateFeatureCommand');
|
|
60
58
|
const verifyCommand = lazy('../src/commands/validation/verify.js', 'verifyCommand');
|
|
61
59
|
const e2eUpCommand = lazy('../src/commands/e2e/e2e.js', 'e2eUpCommand');
|
|
62
60
|
const e2eStatusCommand = lazy('../src/commands/e2e/e2e.js', 'e2eStatusCommand');
|
|
@@ -72,7 +70,6 @@ const contextRefreshCommand = lazy('../src/commands/context/context-refresh.js',
|
|
|
72
70
|
|
|
73
71
|
const dashboardCommand = lazy('../src/commands/dashboard/dashboard.js', 'dashboardCommand');
|
|
74
72
|
|
|
75
|
-
const telemetryCommand = lazy('../src/commands/observability/telemetry.js', 'telemetryCommand');
|
|
76
73
|
const costCommand = lazy('../src/commands/observability/cost.js', 'costCommand');
|
|
77
74
|
|
|
78
75
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -216,12 +213,6 @@ program
|
|
|
216
213
|
.option('--json', 'Output as JSON')
|
|
217
214
|
.action((feature, options) => deleteCommand(feature, options));
|
|
218
215
|
|
|
219
|
-
program
|
|
220
|
-
.command('approval-status <feature>')
|
|
221
|
-
.description('Show approval status for all gates')
|
|
222
|
-
.option('--json', 'Output as JSON')
|
|
223
|
-
.action((feature, options) => approvalStatusCommand(feature, options));
|
|
224
|
-
|
|
225
216
|
program
|
|
226
217
|
.command('unapprove <feature> <gate>')
|
|
227
218
|
.description('Revoke a phase gate approval (recovery only)')
|
|
@@ -324,29 +315,6 @@ graphCommand
|
|
|
324
315
|
.option('--json', 'Output as JSON')
|
|
325
316
|
.action((options) => graphDiffCommand(options));
|
|
326
317
|
|
|
327
|
-
program
|
|
328
|
-
.command('validate [validator]')
|
|
329
|
-
.description('Run project validations (all | packages | next-components | spec-tasks)')
|
|
330
|
-
.option('-v, --verbose', 'Show detailed output')
|
|
331
|
-
.option('--auto-fix, --fix', 'Auto-fix issues where possible')
|
|
332
|
-
.option('--no-fail', 'Don\'t exit with error code')
|
|
333
|
-
.action((validator, options) => {
|
|
334
|
-
const args = validator ? [validator] : ['all'];
|
|
335
|
-
if (options.verbose) args.push('--verbose');
|
|
336
|
-
if (options.autoFix) args.push('--auto-fix');
|
|
337
|
-
if (options.noFail) args.push('--no-fail');
|
|
338
|
-
validateCommand(args);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
program
|
|
342
|
-
.command('validate-feature <feature>')
|
|
343
|
-
.alias('vf')
|
|
344
|
-
.description('Content-aware validation of feature outputs (sections, schema, contracts)')
|
|
345
|
-
.option('--phase <phase>', 'Validate outputs for a specific phase')
|
|
346
|
-
.option('--json', 'Output as JSON')
|
|
347
|
-
.option('-v, --verbose', 'Show detailed output')
|
|
348
|
-
.action(validateFeatureCommand);
|
|
349
|
-
|
|
350
318
|
program
|
|
351
319
|
.command('verify <feature>')
|
|
352
320
|
.description('Run the deterministic verification chain (build + tests + validators + e2e) — code before the LLM judge')
|
|
@@ -361,8 +329,11 @@ program
|
|
|
361
329
|
.option('--skip-build', 'Skip the build node. The run is partial, so the verification stamp is NOT written (reason "skip-build")')
|
|
362
330
|
.option('--skip-e2e', 'Skip the e2e node (Docker bring-up + Playwright)')
|
|
363
331
|
.option('--fresh-e2e', 'Recreate the e2e stack from scratch: down -v (named volumes included), drop the freshness stamp, rebuild, re-seed auth')
|
|
332
|
+
.option('--fresh', 'Run every node again, ignoring the green nodes of an earlier run on this same tree. Use it when something outside the tree changed (.env, database, tool version)')
|
|
364
333
|
.option('--no-persist', 'Do not persist the result to feature.json')
|
|
365
334
|
.option('--ci', 'Push morph/<feature> and take the proof from the project CI (creates .github/workflows/morph-verify.yml when no workflow runs verify). Same stamp, source "ci". No GitHub/gh/workflow = local verify with the reason; uncommitted code = exit 2; CI still running after 50 min = exit 3, no stamp')
|
|
335
|
+
.option('--affected', 'Run only the tests the diff reaches, with build and validators and without e2e. Keeps its own record: the review gate still asks for one complete run. Migration, contract, project-wide files or code shared by too many tests run complete by themselves')
|
|
336
|
+
.option('--detach', 'Run the verify in its own process, out of the calling session: a killed session or background shell does not take the run with it. This call waits and prints the same result; the same command reattaches')
|
|
366
337
|
.action((feature, options) =>
|
|
367
338
|
verifyCommand(feature, { ...options, noPersist: options.persist === false })
|
|
368
339
|
);
|
|
@@ -458,6 +429,12 @@ heavyCommand
|
|
|
458
429
|
.option('--json', 'Output as JSON')
|
|
459
430
|
.action((options) => heavyStatusCommand(options));
|
|
460
431
|
|
|
432
|
+
heavyCommand
|
|
433
|
+
.command('reclaim')
|
|
434
|
+
.description('Derruba stacks e2e ociosas de todos os projetos e containers de teste abandonados; dado de feature ativa fica')
|
|
435
|
+
.option('--json', 'Output as JSON')
|
|
436
|
+
.action((options) => heavyReclaimCommand(options));
|
|
437
|
+
|
|
461
438
|
heavyCommand
|
|
462
439
|
.command('run <command...>')
|
|
463
440
|
.description('Rodar um comando pesado dentro de um slot: morph-spec heavy run [--label x] -- dotnet build. Repassa o exit code; exit 3 = ocupado, não é vermelho')
|
|
@@ -593,14 +570,6 @@ program
|
|
|
593
570
|
.option('--no-open', 'Don\'t auto-open browser')
|
|
594
571
|
.action(dashboardCommand);
|
|
595
572
|
|
|
596
|
-
program
|
|
597
|
-
.command('telemetry')
|
|
598
|
-
.description('Summarize the harness execution path from .morph/logs/events.jsonl (skills, personas, standards read, tool mix, prompts)')
|
|
599
|
-
.option('--json', 'Output the aggregate as JSON')
|
|
600
|
-
.option('--feature <name>', 'Narrow the summary to a single feature')
|
|
601
|
-
.option('--limit <n>', 'How many recent prompts to show (default 10)', (v) => parseInt(v, 10))
|
|
602
|
-
.action((options) => telemetryCommand(options));
|
|
603
|
-
|
|
604
573
|
program
|
|
605
574
|
.command('cost [feature]')
|
|
606
575
|
.description('Cost-facing view over .morph/logs/events.jsonl: bytes re-ingested by kind (proxy, not tokens), dispatch counts, real tokens when observed')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
|
@@ -56,7 +56,6 @@ plan → [uiux] → implement → review
|
|
|
56
56
|
```bash
|
|
57
57
|
morph-spec approve {feature} {gate} [--mode auto] [--approver name] [--notes text]
|
|
58
58
|
morph-spec advance {feature} [--design] # approve current gate + scaffold next phase folder in one step
|
|
59
|
-
morph-spec approval-status {feature} [--json]
|
|
60
59
|
morph-spec unapprove {feature} {gate} [--reason text] # recovery only
|
|
61
60
|
morph-spec finish {feature} --pr|--merge [--push] [--keep] [--force] [--discard-ignored] # normal close: archive + integrate + teardown
|
|
62
61
|
morph-spec archive {feature} [--force] [--no-git] # archive WITHOUT integrating (legacy feature / cleanup)
|
|
@@ -97,8 +96,6 @@ Never hand-edit `gateDecisions`/`approvalGates` in `feature.json` — always go
|
|
|
97
96
|
|
|
98
97
|
```bash
|
|
99
98
|
morph-spec plan-check {feature} [--json] # validate plan.md, write checks.json
|
|
100
|
-
morph-spec validate [packages|next-components]
|
|
101
|
-
morph-spec validate-feature {feature} [--phase {phase}] [--json] # alias: vf
|
|
102
99
|
morph-spec verify {feature} [--json] [--filter <ids>] [--skip-tests] [--skip-build] [--skip-e2e] # deterministic build+test+validator+e2e chain; the gate reads the unfiltered run
|
|
103
100
|
morph-spec report-check {feature} [--before-eval] [--json] # state of the implement and of the Gate 3 reports
|
|
104
101
|
```
|
|
@@ -177,4 +174,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
|
|
|
177
174
|
|
|
178
175
|
---
|
|
179
176
|
|
|
180
|
-
*morph-spec v10.
|
|
177
|
+
*morph-spec v10.5.2 by Polymorphism Tech*
|
package/docs/QUICKSTART.md
CHANGED
package/framework/MORPH.md
CHANGED
|
@@ -10,7 +10,7 @@ Entre gates, você opera sozinho: faz o que `next` mandar (`implement`, `verify`
|
|
|
10
10
|
|
|
11
11
|
| Situação | O que você faz |
|
|
12
12
|
| --- | --- |
|
|
13
|
-
| Ambiguidade que o plano cobre | resolve pelo lado conservador e anota em `## Notas` do `report.md` |
|
|
13
|
+
| Ambiguidade que o plano cobre | resolve pelo lado que serve ao `Resultado buscado` (se um revert não desfaz, pelo conservador) e anota em `## Notas` do `report.md` |
|
|
14
14
|
| Ambiguidade bloqueante (requisito ausente ou contraditório) | uma pergunta via `AskUserQuestion` |
|
|
15
15
|
| `next` devolve `escalate` (3 verify vermelhos seguidos) | `AskUserQuestion`: aceitar, continuar ou parar. Nunca uma 4ª tentativa por conta própria |
|
|
16
16
|
| `verify` exit 3 | fila cheia, não vermelho: no máximo uma chamada nova |
|
package/framework/agents.json
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"prove",
|
|
19
19
|
"scenario",
|
|
20
20
|
"check",
|
|
21
|
-
"mutation",
|
|
22
21
|
"verdict"
|
|
23
22
|
],
|
|
24
23
|
"stack": [
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
],
|
|
33
32
|
"model": "sonnet",
|
|
34
33
|
"maxTurns": 90,
|
|
35
|
-
"persona": "Você é o verificador independente do Gate 3. Não participou da implementação e não recebe o plano, o diff nem o relato de quem implementou — de propósito: sua missão é FAZER cada cenário `C{n}` do `checks.json` acontecer e dizer se o resultado é o que ele promete. O default de todo cenário é NÃO PROVADO; o que você não executou (ou, em `prova: leitura`, não leu no código) nunca vira PROVADO por inferência. Pela `prova:` do Cn: `e2e` → navegue o fluxo com o Playwright MCP na URL do dispatch como um usuário, conferindo tela, console e network, e rode `morph-spec e2e reset {feature}` ANTES de cada fluxo; `teste` → ache o teste do Cn e rode só ele pelo filtro do runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`; `verify --filter` escolhe um nó inteiro e só serve quando o nó é esse teste), depois leia se ele MORDE (nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, assert que passa pelo motivo errado); `eval` → rode o nó de evals filtrado e compare a saída com o cenário; `leitura` → `arquivo:linha` e a fiação da entrada real ao efeito, sem pedir teste; `manual` → reproduza o passo de `3-implement/evidencias/{Cn}.md`. Verificações mecânicas (.NET minimal API / Next): serviço injetado numa lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como constantes distintas. A suíte
|
|
34
|
+
"persona": "Você é o verificador independente do Gate 3. Não participou da implementação e não recebe o plano, o diff nem o relato de quem implementou — de propósito: sua missão é FAZER cada cenário `C{n}` do `checks.json` acontecer e dizer se o resultado é o que ele promete. O default de todo cenário é NÃO PROVADO; o que você não executou (ou, em `prova: leitura`, não leu no código) nunca vira PROVADO por inferência. Pela `prova:` do Cn: `e2e` → navegue o fluxo com o Playwright MCP na URL do dispatch como um usuário, conferindo tela, console e network, e rode `morph-spec e2e reset {feature}` ANTES de cada fluxo; `teste` → ache o teste do Cn e rode só ele pelo filtro do runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`; `verify --filter` escolhe um nó inteiro e só serve quando o nó é esse teste), depois leia se ele MORDE (nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, assert que passa pelo motivo errado); `eval` → rode o nó de evals filtrado e compare a saída com o cenário; `leitura` → `arquivo:linha` e a fiação da entrada real ao efeito, sem pedir teste; `manual` → reproduza o passo de `3-implement/evidencias/{Cn}.md`. Verificações mecânicas (.NET minimal API / Next): serviço injetado numa lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como constantes distintas. A suíte completa roda depois de você: nunca `verify` sem filtro nem a suíte completa (o hook `test-budget` nega). Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — não suba nem derrube o stack. Siga a seção \"Verificador\" de `.morph/framework/evals/review-checklist.md` e grave `4-review/verifier.md` com `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, uma linha por cenário; a evidência é o que você executou e viu) — mais `## Checklist de tela` e `## Mídia gerada` quando o checklist pede. NÃO PROVADO ou FALHA vira bloqueador `check` (ou `teste`). Arquivo você lê com Read, nunca `sed`/`cat`/`head`. NUNCA modifique código, plan.md, checks.json ou feature.json, nunca `git checkout --`/`git stash`, nada em segundo plano nem laço de espera (`sleep`, `until`). Sem grafo (`no-graph`), caia para grep."
|
|
36
35
|
},
|
|
37
36
|
"reviewer": {
|
|
38
37
|
"title": "Gate 3 Reviewer",
|
|
@@ -23,7 +23,7 @@ is idempotent: an already-archived feature is reported, not re-moved.
|
|
|
23
23
|
|
|
24
24
|
1. **Verify** the feature cleared Gate 3:
|
|
25
25
|
```
|
|
26
|
-
morph-spec
|
|
26
|
+
morph-spec status {feature} --verbose
|
|
27
27
|
```
|
|
28
28
|
Gate `review` must be `approved`. If not, stop and report what's missing (don't force it without
|
|
29
29
|
the user's say-so).
|
|
@@ -26,15 +26,14 @@ These checks verify that the morph-spec workflow is complete for the feature bei
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Verify implementation is complete
|
|
29
|
-
morph-spec status {feature}
|
|
30
|
-
morph-spec approval-status {feature}
|
|
29
|
+
morph-spec status {feature} --verbose
|
|
31
30
|
morph-spec verify {feature}
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
| Check | Command | Pass Criteria |
|
|
35
34
|
|-------|---------|---------------|
|
|
36
35
|
| Phase is implement or later | `status` | phase derived as `implement` or `review` |
|
|
37
|
-
| All gates approved | `
|
|
36
|
+
| All gates approved | `status --verbose` | plan (and uiux, when the feature has design) and review gates approved |
|
|
38
37
|
| Build, tests and validators pass | `verify` | every node green |
|
|
39
38
|
| All slices reported | `status` → Fatias | 0 slices pending or partial |
|
|
40
39
|
|
|
@@ -42,7 +41,7 @@ morph-spec verify {feature}
|
|
|
42
41
|
|
|
43
42
|
---
|
|
44
43
|
|
|
45
|
-
**Tests:**
|
|
44
|
+
**Tests:** the proof the gates read is `morph-spec verify {feature}` without `--filter`. A bare `dotnet test`/`npm test` is fine for checking something (it goes through the heavy-job queue), but it leaves no proof. Any red node blocks deployment.
|
|
46
45
|
|
|
47
46
|
---
|
|
48
47
|
|
package/framework/docs/CLI.md
CHANGED
|
@@ -64,7 +64,7 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
|
|
|
64
64
|
| `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo, prints 1280/390, ou seletor e print de cada estado. **ship**: item decidido sem linha em `## Checklist de tela` |
|
|
65
65
|
| `planChecks` | ≠ `fresh` | `checks.json` ausente ou de outra versão do `plan.md`: rode `plan-check`. Só no **plano**; `approve`/`advance` automáticos recusam |
|
|
66
66
|
| `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
|
|
67
|
-
| `planCritique` | `missing`/`no-hash`/`stale` | `2-plan/critica.md` ausente, sem `**Plan-Hash:**`, ou de outra versão do plano sem `**Aplicado-em:**` que cubra a atual (`signal: plan-critique`). Só no **plano**, `chore`/`hotfix` isentos (`exempt`); `approve`/`advance` automáticos recusam |
|
|
67
|
+
| `planCritique` | `missing`/`no-hash`/`stale` | `2-plan/critica.md` ausente, sem `**Plan-Hash:**`, ou de outra versão do plano sem `**Aplicado-em:**` que cubra a atual (`signal: plan-critique`). Só no **plano**, `chore`/`hotfix` e plano pequeno isentos (`exempt`: até 3 cenários, todos `prova: teste` ou `leitura`, sem degrau e sem mídia gerada); `approve`/`advance` automáticos recusam |
|
|
68
68
|
| `rungs` | `Cn` com `degrau: migrar` ou `irreversível` (`checks.json`) | **plano**: pausa nomeando os `Cn` (`signal: rung`), e `approve`/`advance` automáticos recusam. **ship**: pausa se o plano não teve aprovação humana (chore, policy) |
|
|
69
69
|
| `migrationsTouched` | caminho `migrations/` no diff, `rungs` vazio | Feature mexeu em dado sem declarar o degrau (`signal: rung-undeclared`). Só no **ship** |
|
|
70
70
|
| `contractsTouched` | `openapi.{yaml,yml,json}`, `swagger.{…}` ou `.proto` no diff, sem `Cn` `irreversível` | O arquivo de contrato mudou sem o degrau (`signal: rung-undeclared`). Webhook e endpoint ficam com o bloqueador `irreversivel` do revisor. Só no **ship** |
|
|
@@ -125,7 +125,7 @@ Feature sem `workType` (pré-v8) é tratada como `feature`.
|
|
|
125
125
|
|
|
126
126
|
### finish
|
|
127
127
|
|
|
128
|
-
**`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
|
|
128
|
+
**`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum; a sessão escolhe por `finish.mode` do `config.json`, padrão `pr`): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
|
|
129
129
|
|
|
130
130
|
**Índice de ADRs:** no mesmo commit do archive, o `finish` lê o `2-plan/plan.md` da feature: os ADRs de `## Decisões` com `**Alcance:** projeto` entram em `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo), e a tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` retira (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os ADRs revogados — forma em `morph-spec plan-check --help`; o `plan-check` avisa, enquanto o plano ainda muda, a linha que o `finish` não aplicaria. O payload traz `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`.
|
|
131
131
|
|
|
@@ -277,7 +277,11 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
|
|
|
277
277
|
|
|
278
278
|
### verify
|
|
279
279
|
|
|
280
|
-
**`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira. Antes da cadeia, instala em `.claude/rules/` (fora do git) a regra de stack que falta, pela stack do `config.json` somada à que os arquivos mostram (em stderr; nunca muda o veredito). Nunca grava o `config.json`: arquivo versionado mudado pelo próprio verify tornaria a prova velha e a árvore suja para o `--ci`; a tag nova entra pelo `plan-check` ou pelo `update`.
|
|
280
|
+
**`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--fresh] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira. Antes da cadeia, instala em `.claude/rules/` (fora do git) a regra de stack que falta, pela stack do `config.json` somada à que os arquivos mostram (em stderr; nunca muda o veredito). Nunca grava o `config.json`: arquivo versionado mudado pelo próprio verify tornaria a prova velha e a árvore suja para o `--ci`; a tag nova entra pelo `plan-check` ou pelo `update`.
|
|
281
|
+
|
|
282
|
+
**Reaproveitamento entre rodadas:** uma rodada completa grava o resultado por nó em `.git/morph-spec/verify-nodes-<feature>.json`, chaveado pela impressão da árvore (a régua de `treeFingerprint`). Mesma árvore: build, suíte e e2e verdes não rodam de novo (`reused[]`; os validadores sempre rodam), e o vermelho que passa agora entra em `flaky[]`, que o stamp guarda em `verification.flaky` para o verificador. Árvore mudada: nada é aproveitado; as suítes vermelhas da rodada anterior rodam primeiro e, se uma seguir vermelha, as outras ficam `skip` (`adiado: …`) e a rodada sai vermelha. O stamp verde sempre exige todos os nós verdes na árvore atual. Rodada parcial (`--filter`, `--skip-*`, `--linux`, `--record-baseline`, `--refresh-cache`, `--fresh-e2e`) não lê nem grava. O que a impressão não vê (`.env`, banco, versão de ferramenta): `--fresh` roda tudo.
|
|
283
|
+
|
|
284
|
+
**Reexecução de spec que falhou:** com Playwright (`npx playwright test`), specs vermelhas rodam mais uma vez com `--last-failed` (exige Playwright 1.44+). Passaram: nó verde, com os nomes em `flaky[]`. Falharam: nó vermelho. `e2e.command` próprio sem `playwright` como argumento não reexecuta. Cada rodada de specs tem teto de `e2e.specTimeoutMs` (padrão 300 000 ms).
|
|
281
285
|
|
|
282
286
|
O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável, sobe os `e2e.hostServices` (ver `e2e` abaixo) 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). Sem compose mas com `e2e.hostServices`, a stack é **só de host**: sobem só os serviços do host, sem lint de isolamento do compose nem checagem de Docker, e as specs rodam contra eles. Sem compose e sem `e2e.hostServices`, o nó pula com `project does not containerize (no compose file)`; com a integração `docker` declarada e sem compose, falha.
|
|
283
287
|
|
|
@@ -311,10 +315,22 @@ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de con
|
|
|
311
315
|
|
|
312
316
|
**Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
|
|
313
317
|
|
|
314
|
-
**Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e
|
|
318
|
+
**Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho: `morph-spec heavy status` mostra quem segura a vaga, e cabe no máximo uma chamada nova.
|
|
315
319
|
|
|
316
320
|
**Por quê.** Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar.
|
|
317
321
|
|
|
322
|
+
### verify --affected
|
|
323
|
+
|
|
324
|
+
**`morph-spec verify <feature> --affected [--json] [--detach]`** — a rodada do dia a dia: build, validadores e só os testes que o diff contra o merge-base com a branch padrão alcança, sem e2e. Grava em `verifications.affected` (com `sha` e `failStreak`) e abre o dispatch do Gate 3, mas **nunca substitui o `verification`** que o gate lê: `gate-check review` segue exigindo uma rodada completa, que a skill faz uma vez, depois dos relatórios.
|
|
325
|
+
|
|
326
|
+
**Sobe sozinha para a completa**, com a razão no `feedback`, e aí carimba `verification` normalmente: migration, contrato (`openapi`, `swagger`, `.proto`), arquivo que alcança o projeto inteiro (`package.json`, lockfile, `.csproj`, `Directory.*.props`, `global.json`, `tsconfig`, `vitest.config`, fora dos projetos de teste), código que alcança mais de 40 classes de teste, ou sem base de comparação.
|
|
327
|
+
|
|
328
|
+
**Seleção por nó:** `vitest` ganha `--changed <base> --passWithNoTests`; `dotnet` filtra pelas classes de teste que citam os tipos mudados (`git grep -lw`, um salto só) mais os testes mudados; nó `e2e` e `evals` (também o `dotnet` chamado `evals`, porque custa dinheiro) ficam para a completa; os outros rodam inteiros se algo mudou no `cwd` deles. Nó sem nada a rodar sai pulado. **Nada a configurar:** filtro já no comando é combinado com a seleção; `.ps1` com `[string] $Filter` que chega ao `dotnet test` usa `-Filter` e guarda o padrão do parâmetro; script que repassa os argumentos (`"$@"`, `$args`) recebe `--filter`. Só um comando que nada disso alcança roda inteiro, ou declara `affected: { flag, baseFilter }` em `project.tests[]`, que vence a detecção. Só doc ou imagem no diff: pula build e testes. Não combina com `--ci`, `--filter`, `--record-baseline`, `--refresh-cache` nem `--linux`.
|
|
329
|
+
|
|
330
|
+
### verify --detach
|
|
331
|
+
|
|
332
|
+
**`morph-spec verify <feature> [--json] --detach`** — roda o `verify` num processo fora da árvore da sessão: a chamada espera e devolve a mesma saída e o mesmo exit, mas uma sessão encerrada, ou o Claude Code matando comandos de fundo sob pressão de memória, não leva a rodada junto, e o stamp é gravado do mesmo jeito. Um processo lançador nasce e morre entre os dois, para que quem mata por árvore não alcance a rodada. O mesmo comando sobre a mesma árvore **retoma** (rodada viva: espera; terminada e não lida: devolve uma vez). Outra árvore com rodada viva da feature: espera ela acabar e abre a sua. Rodada que morreu sem resultado: exit 3, sem stamp, nunca vermelho. Estado em `verify-<id>.{json,out,err,exit}` no diretório do `heavy`. Quem contém a RAM é o piso antes de cada nó e o vigia durante a rodada (`### heavy`).
|
|
333
|
+
|
|
318
334
|
### verify --ci
|
|
319
335
|
|
|
320
336
|
**`morph-spec verify <feature> --ci [--json]`** — a prova de escopo de feature tirada do **CI do projeto** (GitHub Actions), não da máquina: faz `git push -u origin morph/<feature>`, espera o run do commit e transforma o `verify --json` que o run publicou (artefato `morph-verify`) no **mesmo stamp** de uma rodada local, com `source: "ci"` e `ciRun` (URL do run); `sha` é o `headSha` do run, então o `verificationStale` compara o código que o CI viu com o HEAD.
|
|
@@ -325,7 +341,7 @@ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
|
|
|
325
341
|
|
|
326
342
|
**Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de tudo — com ou sem CI — reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não empurra, não roda nem regrava, imprime "stamp verde e fresco — o CI não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
|
|
327
343
|
|
|
328
|
-
Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com
|
|
344
|
+
Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
|
|
329
345
|
|
|
330
346
|
**O runner só tem o que está commitado:** o que testes e e2e leem de arquivo gitignored (`.env`, `appsettings.*.json` local, connection string do Neon, `storageState`) não existe lá, e o run sai vermelho sem defeito de código. Declare o valor como secret do repositório e exponha-o no `env:` do job em `morph-verify.yml` (o arquivo é do projeto: editar à mão é o esperado, e o `findCiWorkflow` continua aceitando enquanto ele chamar `morph-spec verify` publicando `morph-verify.json`), ou desligue com `project.ci: false`.
|
|
331
347
|
|
|
@@ -341,7 +357,7 @@ Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.s
|
|
|
341
357
|
|
|
342
358
|
Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro.
|
|
343
359
|
|
|
344
|
-
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora
|
|
360
|
+
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora no plano, junto do `Cn`, e é lá que o revisor a lê.
|
|
345
361
|
|
|
346
362
|
### e2e
|
|
347
363
|
|
|
@@ -398,21 +414,29 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
|
|
|
398
414
|
|
|
399
415
|
**Piso de RAM por tipo** (`heavy.minFreeMemGbByKind`, padrão `unit` 1,5 · `build` 4 · `e2e` 4 GB): `unit` = vitest/jest, `npm|pnpm|yarn test`, `npm run test[:x]` e o nó `tests` do verify; `e2e` = playwright, `docker compose up`, script com `e2e` no nome, o nó `e2e` e o `e2e up`; `build` = o resto (`dotnet build|test|publish` — o `dotnet test` compila —, `next build`, `docker build` e comando desconhecido). O `heavy run` e o hook do `gateBash` classificam pelo comando; o `verify` toma o slot com o piso do primeiro nó e, antes de um nó mais pesado (tests → e2e), espera de novo só pela RAM segurando o slot — estourar ali também é exit 3, sem stamp. `heavy.minFreeMemGb` (4) virou o fallback: **definido explicitamente**, vale para todo tipo que `minFreeMemGbByKind` não lista (`0` continua desligando a checagem em tudo); sem ele, cada tipo usa o seu padrão.
|
|
400
416
|
|
|
417
|
+
**Reclaim antes de esperar** (`verify`, `e2e up`, `heavy run`, o hook `heavy-gate` e `morph-spec heavy reclaim [--json]` à mão; `heavy.reclaimIdleMinutes`, 20, `0` desliga; `MORPH_HEAVY_RECLAIM=off` desliga por ambiente, e a suíte do framework roda assim): abaixo do piso, uma vez por rodada, derruba as stacks e2e **de todos os projetos registrados** que ninguém usa — a de feature arquivada, com volumes; a de feature ativa sem vaga na fila e sem evento em `.morph/logs/events.jsonl` há esse tempo, **mantendo os volumes** (o próximo `e2e up` só recompila) —, remove os containers de teste de `heavy.reclaimContainers` (`morph-pgtest`) quando nenhuma vaga pode estar testando, e encerra os build-servers ociosos do .NET. Nunca toca a stack da própria feature, nem stack sem dono, nem a de atividade desconhecida.
|
|
418
|
+
|
|
419
|
+
**Vigia durante o `verify --detach`** (`heavy.criticalFreeMemGb`, 1, `0` desliga): o piso julga o começo de um nó; o vigia, um processo próprio, lê a RAM a cada 10 s durante a rodada. Abaixo do valor, tenta o reclaim na 1ª leitura; seis leituras seguidas abaixo param **só aquela rodada**, com a árvore de processos dela: exit 3, sem stamp, nunca vermelho.
|
|
420
|
+
|
|
421
|
+
**Máquina responsiva** (`heavy.yieldToUser`, ligado): cada rodada usa só a sua parte dos núcleos (`núcleos / heavy.slots`, mínimo 2) por `DOTNET_PROCESSOR_COUNT` (build e testes) e `--maxWorkers` no Vitest. Prioridade baixa fica de fora de propósito: numa máquina ocupada, o processo perde a disputa e falha pelo próprio limite de tempo. O preço é rodada um pouco mais lenta. `DOTNET_PROCESSOR_COUNT` já definido no ambiente vence. O hook `heavy-gate` toma vaga, mas não muda o comando do Bash: lá isso não se aplica.
|
|
422
|
+
|
|
423
|
+
**Fila que não recusa** (`heavy.detachedWaitMinutes`, 60): fora de um shell não há timeout de chamada, então o `verify --detach` espera a vez em vez de sair com exit 3 em `waitTimeoutSec`. `MORPH_HEAVY_WAIT_SEC` sobrepõe a espera.
|
|
424
|
+
|
|
401
425
|
**Ocupado por RAM** (`verify`, `e2e up`, `heavy run`): a mensagem nomeia o piso e a chave (`heavy.minFreeMemGbByKind.<tipo>`) e lista as stacks e2e de pé deste repo (`docker compose ls`, prefixo `morph-`, compose sob a raiz principal — de todas as worktrees). Feature já arquivada (`.morph/archive/<f>` sem `.morph/features/<f>`) vem com `morph-spec e2e down <f>`; stack sem feature, com `docker compose -p <nome> down`. Docker fora ou lento (10 s) = lista vazia, nunca erro.
|
|
402
426
|
|
|
403
427
|
**Estouro = exit 3 (`busy`), nunca vermelho:** `verify --json` sai `{ status: "busy", persisted: false, busy: { reason, holders, freeGb, waitedMs } }`, **sem stamp e sem evento `Verify`** (nada foi verificado, e fila não é gate verde nem vermelho); `e2e up` e `heavy run` também saem 3. Quem orquestra repete a chamada — nas skills, exit 3 não conta como tentativa e não dispara o loop de correção.
|
|
404
428
|
|
|
405
429
|
**Slot vago:** pid morto, ou idade acima de `heavy.maxHoldMinutes` (90, contra reuso de PID); Ctrl-C deixa o arquivo, que o pid morto cobre. Um filho herda o slot por `MORPH_HEAVY_LEASE` e nunca pede um segundo.
|
|
406
430
|
|
|
407
|
-
**Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `minFreeMemGbByKind`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado.
|
|
431
|
+
**Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `minFreeMemGbByKind`, `waitTimeoutSec`, `maxHoldMinutes`, `reclaimIdleMinutes`, `reclaimContainers`, `detachedWaitMinutes`, `criticalFreeMemGb`, `yieldToUser`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado.
|
|
408
432
|
|
|
409
433
|
Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`.
|
|
410
434
|
|
|
411
|
-
**Fim do trabalho pesado:** no fim do `verify`
|
|
435
|
+
**Fim do trabalho pesado:** no `e2e down`, no `doctor --procs --prune` e no fim do `verify` quando a RAM livre está abaixo do dobro de `heavy.minFreeMemGb` (com folga, o compilador fica quente para a próxima rodada), roda `dotnet build-server shutdown` **só** se o projeto é .NET, `heavy.shutdownBuildServer` está ligado, nenhum slot está preso **e** nenhum `dotnet build|test|publish|msbuild` está vivo — sub-agents e IDE não passam pelo semáforo, então semáforo vazio não basta. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`.
|
|
412
436
|
|
|
413
437
|
`heavy.disableNodeReuse` põe `MSBUILDDISABLENODEREUSE=1` enquanto o nó pesado roda e restaura depois. O `--linux` rotula o container (`morph-spec.verify=<runId>`, `morph-spec.root=<hash da raiz>`) e o remove **por label** no fim, nunca por nome; o `e2e down` usa `--remove-orphans`. O `verify` **não** derruba a stack e2e (`keepUp` é por desenho).
|
|
414
438
|
|
|
415
|
-
**Bash cru (`heavy.gateBash`, padrão `true`):** a sessão roda build e teste quando quer, e um `dotnet build`/`dotnet test`/`npm test` pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda.
|
|
439
|
+
**Bash cru (`heavy.gateBash`, padrão `true`):** a sessão roda build e teste quando quer, e um `dotnet build`/`dotnet test`/`npm test` pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build`, `powershell|pwsh` com um `.ps1` cujo nome contém `test` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda.
|
|
416
440
|
|
|
417
441
|
`heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`.
|
|
418
442
|
|
|
@@ -496,11 +520,13 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
496
520
|
|
|
497
521
|
**Bloco `tree`**, em toda saída e sem mudar o exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde — desconhecido, nunca árvore limpa.
|
|
498
522
|
|
|
499
|
-
**`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`.
|
|
523
|
+
**`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`. Traz ainda `gate3: { base, sha, flaky, e2eChecks, redispatch }`: `base` é o merge-base com a default, `flaky` vem do stamp do verify, `e2eChecks` lista os `Cn` de `prova: e2e`, e `redispatch` diz quem volta depois de uma correção (`null` sem relatório: todos os exigidos vão).
|
|
524
|
+
|
|
525
|
+
**Quem volta depois de uma correção** (`reviewConfirmation`, a mesma regra no `approve`): papel com relatório atrás do `HEAD` volta, salvo SHIP limpo com delta estreito: arquivos citados nos bloqueadores (caminho ou nome) ou de teste; para a segurança, nenhum caminho sensível. O `approve` só dispensa esse papel com um papel que abriu bloqueador revisando o `HEAD`.
|
|
500
526
|
|
|
501
527
|
### plan-check
|
|
502
528
|
|
|
503
|
-
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `2-plan/plan.html`, a visão só leitura que o gate do plano abre no navegador: cenários com prova/risco/degrau, padrões assumidos numerados e, com `## Estados` (opcional, `- origem → destino: gatilho` por linha), a máquina de estados desenhada. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`,
|
|
529
|
+
**`morph-spec plan-check <feature> [--json]`** — lê `2-plan/plan.md` com o parser único (o `morph-spec create` já escreve o esqueleto; a gramática completa sai em `morph-spec plan-check --help`) e, sem erro, grava `2-plan/checks.json` — o único escritor dele (o `protect-spec-files` recusa Write/Edit à mão) — e `2-plan/plan.html`, a visão só leitura que o gate do plano abre no navegador: cenários com prova/risco/degrau, padrões assumidos numerados e, com `## Estados` (opcional, `- origem → destino: gatilho` por linha), a máquina de estados desenhada. **Erros** (exit 1, nada gravado): seção obrigatória ausente (`Problema`, `Cenários`, `Decisões` com `### Conflitos com o acervo` não vazio — "nenhum" vale); `## Errata` fora do fim; `## Cenários` sem cenário; cenário sem `prova:` ou fora de `teste|e2e|eval|manual|leitura`; `prova: teste` sem `risco:` conhecido; `risco:` com `prova: leitura`; cenário com vocabulário de artefato sem `> Por que artefato:` logo abaixo; id `C`/`ADR` duplicado; `bug`/`hotfix` sem cenário de regressão provado por `teste` ou `e2e`; plano que fala em mídia gerada por IA sem citar `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` nem declarar a linha `Mídia gerada: não` (sem a citação, as provas de mídia do Gate 3 ficariam desligadas em silêncio); veredito de reuso fora de `REUSAR|ESTENDER|CRIAR`; `## Estados` vazia ou com linha que não é transição. **Avisos:** `CRIAR` sem `why`, ADR sem `**Alcance:** feature|projeto`, `## Fatias` que sobrou da 9.x (não é lida). Com o gate `plan` aprovado e o corpo mudado fora da `## Errata`, recusa (`frozen`, exit 1) — o `planHash` ignora a errata; mudança de escopo é `unapprove <f> plan`, `plan-check` e o gate de novo. Plano extraído antes das regras de `risco:` e de mídia não passa a falhar por elas. Exit 2: `plan.md` ausente. Fora da árvore dona da feature, recusa nomeando-a. Com o plano válido, acrescenta ao `config.json` a stack que os arquivos mostram e instala em `.claude/rules/` a regra de stack que falta (uma linha por regra, em stderr; `rulesAdded` no `--json`).
|
|
504
530
|
|
|
505
531
|
### stack-set
|
|
506
532
|
|
|
@@ -545,6 +571,7 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
|
|
|
545
571
|
| `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
|
|
546
572
|
| `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
|
|
547
573
|
| `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
|
|
574
|
+
| `parallel` | não | `true` roda o nó junto dos outros nós com `parallel: true`. Declare só em suítes que não dividem banco, porta nem pasta de build: o harness não tem como conferir. Ignorado em nó `e2e: true` e numa rodada que persegue suíte vermelha |
|
|
548
575
|
| `expectedCount` | não | Baseline, só nos runners de contagem estável (`node-test`, `evals`); nos outros é ignorada e o `--record-baseline` a apaga. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
|
|
549
576
|
| `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
|
|
550
577
|
| `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
|
|
@@ -691,8 +718,8 @@ testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou d
|
|
|
691
718
|
**Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
|
|
692
719
|
os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
|
|
693
720
|
antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
|
|
694
|
-
`test-build-failed` e a suíte **não roda**. E um `command` com `--no-build` que **não** foi precedido de
|
|
695
|
-
build nesta invocação reprova com `stale-binary`, também sem executar: o predicado é "houve build
|
|
721
|
+
`test-build-failed` e a suíte **não roda**. E um `command` `dotnet` com `--no-build` que **não** foi precedido de
|
|
722
|
+
build nesta invocação (prebuild do nó ou `dotnet build` verde do nó `build`) reprova com `stale-binary`, também sem executar: o predicado é "houve build
|
|
696
723
|
aqui", nunca "a flag está ausente". Sem isso, um projeto de teste que deixou de compilar era executado
|
|
697
724
|
a partir do binário do último build bom e reportava verde. `--no-build` continua legítimo — com
|
|
698
725
|
`build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito para isso. O prebuild é
|
|
@@ -721,10 +748,10 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
|
|
|
721
748
|
| CI | a suíte inteira, todos os nós (`morph-verify.yml` roda o mesmo `verify {f}`) | idem — e o artefato dele é o stamp do Gate 3 |
|
|
722
749
|
| Evidência de eval (cenário `Cn` com `prova: eval`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals`, sem stack e sem carimbo — é a evidência de runtime do cenário, não a verificação da feature |
|
|
723
750
|
|
|
724
|
-
> **
|
|
725
|
-
>
|
|
726
|
-
> relógios que o
|
|
727
|
-
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms
|
|
751
|
+
> **Nenhum relógio do `verify` é de 600 s.** Os 600 s que existem são a espera da fila
|
|
752
|
+
> (`heavy.waitTimeoutSec`) e o teto do **Bash do harness**, externo ao morph-spec: é ele que corta uma
|
|
753
|
+
> suíte longa, e não há o que "subir". Os relógios que o `verify` de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
|
|
754
|
+
> encadeado**, `up --build` e health do e2e **180 000 ms**, specs Playwright **300 000 ms** (`e2e.specTimeoutMs`), mais `timeoutMs` por nó,
|
|
728
755
|
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
729
756
|
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
|
|
730
757
|
> `verify {feature}` com `run_in_background` e
|
|
@@ -828,10 +855,6 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
828
855
|
|
|
829
856
|
## Observabilidade
|
|
830
857
|
|
|
831
|
-
### telemetry
|
|
832
|
-
|
|
833
|
-
**`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, sub-agents despachados + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log`. Lê também os arquivos rotacionados em `.morph/logs/sessions/`, sem contar duas vezes a mesma linha.
|
|
834
|
-
|
|
835
858
|
### cost
|
|
836
859
|
|
|
837
860
|
**`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`checklist`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
|
|
@@ -866,6 +889,6 @@ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit cod
|
|
|
866
889
|
| `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
|
|
867
890
|
| `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
|
|
868
891
|
| `Gate` | `gate`, `step` (`decision`/`approve`/`unapprove`); na `decision`: `pause`, `signal` (a regra que pausou, `null` sem pausa, `manual` pelo `gate-decision`) e `source`; nas outras: `approvedBy`. Gravado por `persistGateDecision` e pelos dois escritores de aprovação (CLI e hook `state-sync`) |
|
|
869
|
-
| `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
|
|
892
|
+
| `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.tests.runs[].{id,status,durationMs}` (qual suíte custou o quê), `reused[]`, `flaky[]`, `nodes.validators.{ran,skipped}` |
|
|
870
893
|
|
|
871
894
|
Nada da saída de um comando é gravado — nem o corpo de erro do `PostToolUseFailure`, onde um valor de `.env` pode aparecer sem nome ao lado. `command`, `prompt`, `desc` e `target` passam por redação de segredos óbvios antes de gravar: valor ao lado de um nome de segredo (`API_KEY=…`, `*_KEY`/`*_PASS=…`, `--password …`, `user-secrets set <k> <v>`, `AccountKey=…`), `Authorization: …`, `user:pass@` (senha com `/` inclusive) e `?sig=`/`?access_token=` em URL, e formatos de token (GitHub, `sk-`, `sk_live_`, `npm_`, Slack, AWS, Google, JWT). É por padrão, não por garantia: um valor sem nome ao lado passa.
|
|
@@ -33,7 +33,7 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
|
|
|
33
33
|
| `check` | verificador | cenário `Cn` NÃO PROVADO ou FALHA | o que falta provar, ou o repro que falha |
|
|
34
34
|
| `teste` | verificador | teste vermelho; teste que não morde a regra que diz proteger; Cn de risco sem teste | saída do teste filtrado, assert que passa pelo motivo errado |
|
|
35
35
|
| `regressao` | revisor | comportamento que funcionava e quebrou | consumidor afetado + o caminho que quebra |
|
|
36
|
-
| `escopo` | revisor | o diff faz o que nenhum cenário
|
|
36
|
+
| `escopo` | revisor | o diff faz o que nenhum cenário nem o `Resultado buscado` explica, deixa de fazer o que o contrato pede, ou o que você notou contradiz o `Resultado buscado` | trecho sem explicação, o item do contrato ausente, ou o fato e a frase do Resultado que ele desmente |
|
|
37
37
|
| `arquitetura` | revisor | padrão ou abstração nova contra o stack e os standards; item da lista fechada de `architecture-engineering-lens#review-blockers` que o DIFF introduziu (o que já existia é nota) | o trecho e o standard que ele contraria |
|
|
38
38
|
| `performance` | revisor | custo que o volume real torna visível | a consulta/lista e o volume que a derruba |
|
|
39
39
|
| `irreversivel` | revisor | o diff faz o que um revert não desfaz e nenhum `Cn` declara `degrau: migrar`/`irreversível`: apaga ou reescreve dado fora de migration, envia a terceiro, cobra, muda resposta de API ou webhook que alguém consome | o trecho e o efeito que fica depois do revert. Vai direto ao usuário, sem rodada de correção |
|
|
@@ -64,7 +64,7 @@ linha de cada Cn.
|
|
|
64
64
|
|
|
65
65
|
| `prova:` | Execução |
|
|
66
66
|
|---|---|
|
|
67
|
-
| `e2e` | Navegue o fluxo com o Playwright MCP na URL do dispatch, como o usuário faria; confira o resultado na tela, no console e no network. Rode `morph-spec e2e reset {feature}` **antes de cada fluxo**: estado de um fluxo nunca prova o seguinte. |
|
|
67
|
+
| `e2e` | Navegue o fluxo com o Playwright MCP na URL do dispatch, como o usuário faria, disparando a ação até o efeito (controle presente não prova nada); confira o resultado na tela, no console e no network. Rode `morph-spec e2e reset {feature}` **antes de cada fluxo**: estado de um fluxo nunca prova o seguinte. |
|
|
68
68
|
| `teste` | Ache o teste que cobre o Cn e rode **só ele**, pelo filtro do próprio runner (`dotnet test --filter <nome>`, `node --test <arquivo>`, `npx vitest run <arquivo> -t <nome>`, `npx playwright test <arquivo>`). `verify --filter` escolhe um nó inteiro da suíte: só serve quando o nó é esse teste. Verde não basta: leia o teste — ele morde? Nasceu afirmando o que já era verdade, só testa a recusa e nunca a escrita, ou o assert passa pelo motivo errado → bloqueador `teste`. Sem teste para um Cn com `risco:` → `teste`. |
|
|
69
69
|
| `eval` | Rode o nó de evals filtrado (`--filter <nó-evals> --skip-e2e --no-persist`) e compare a saída com o cenário. |
|
|
70
70
|
| `leitura` | `arquivo:linha` do código que entrega o cenário e a **fiação**: caminho da entrada real ao efeito (a camada pura não prova que o handler a chama). Sem teste, e pedir teste não é bloqueador. |
|
|
@@ -74,8 +74,8 @@ Verificações mecânicas valem para todo Cn de backend (.NET minimal API / Next
|
|
|
74
74
|
lambda de endpoint registrado em `Program.cs`; prefixo de rota server-direct e de proxy como
|
|
75
75
|
constantes distintas.
|
|
76
76
|
|
|
77
|
-
Você não roda a suíte inteira nem `verify` sem filtro (o hook `test-budget` nega):
|
|
78
|
-
|
|
77
|
+
Você não roda a suíte inteira nem `verify` sem filtro (o hook `test-budget` nega): antes do dispatch
|
|
78
|
+
rodaram os testes que o diff alcança, e a suíte completa roda depois dos relatórios. Stack fora do ar ou URL ausente: os `e2e` ficam NÃO PROVADO com o motivo — achado de
|
|
79
79
|
infra, não defeito seu a provar; não suba nem derrube o stack.
|
|
80
80
|
|
|
81
81
|
**Feature que gera mídia** (o `plan.md` cita um standard de mídia): grave `## Mídia gerada`, uma
|
|
@@ -101,7 +101,7 @@ havendo item decidido, o gate pausa.
|
|
|
101
101
|
Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}:<caminho>`, e o
|
|
102
102
|
`2-plan/plan.md` (a spec). Nunca a árvore de trabalho: o commit é o que foi provado. Default SHIP.
|
|
103
103
|
|
|
104
|
-
1. **Escopo**: o diff contra os cenários e o `## Contrato de implementação` do plano.
|
|
104
|
+
1. **Escopo**: o diff contra os cenários, o `Resultado buscado` e o `## Contrato de implementação` do plano.
|
|
105
105
|
Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
|
|
106
106
|
risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
|
|
107
107
|
camadas — bloqueador `escopo` (apague, não acrescente). Teste apagado sem porquê nas `## Notas` do
|
|
@@ -33,7 +33,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
33
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 |
|
|
34
34
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
35
35
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions |
|
|
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
|
|
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 cost` |
|
|
37
37
|
| **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
|
|
38
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
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` |
|
|
@@ -41,7 +41,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
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 |
|
|
43
43
|
| **UserPromptSubmit** | telemetry-log.js | Telemetry | Appends the user's prompt to the same `events.jsonl` stream; silent, fail-open |
|
|
44
|
-
| **Stop** | validate-completion.js | Advisory | Warns about
|
|
44
|
+
| **Stop** | validate-completion.js | Advisory | Warns about scenarios marked `feito` without evidence in `3-implement/report.md` (hash-deduped, only re-injects when the warning set changes) |
|
|
45
45
|
|
|
46
46
|
## Design Principles
|
|
47
47
|
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
// The lease has no live pid (the hook exits at once), so it lapses by TTL; a call that fires no Post hook holds it until then.
|
|
4
4
|
// The wait stays under the 600 s hook timeout because a timed-out PreToolUse hook lets the call run.
|
|
5
5
|
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { freemem } from 'node:os';
|
|
6
8
|
import { stateExists } from '../../shared/state-reader.js';
|
|
7
9
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
8
10
|
import { heavyClass, heavyKind, leaseTtlMs } from '../../shared/heavy-command.js';
|
|
9
11
|
|
|
10
12
|
const HOOK_BUDGET_SEC = 540;
|
|
13
|
+
const RECLAIM_TIMEOUT_MS = 45_000;
|
|
14
|
+
|
|
15
|
+
const defaultFreeGb = () => freemem() / 1024 ** 3;
|
|
16
|
+
|
|
17
|
+
// Through the CLI: an installed hook cannot reach the package's src/.
|
|
18
|
+
function runReclaim(cwd, env) {
|
|
19
|
+
if (env.MORPH_HEAVY_RECLAIM === 'off') return;
|
|
20
|
+
spawnSync('morph-spec heavy reclaim --json', {
|
|
21
|
+
cwd, env, shell: true, windowsHide: true, stdio: 'ignore', timeout: RECLAIM_TIMEOUT_MS,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export async function run(payload, deps = {}) {
|
|
13
26
|
try {
|
|
@@ -27,6 +40,12 @@ export async function run(payload, deps = {}) {
|
|
|
27
40
|
if (!config.enabled || !config.gateBash) return null;
|
|
28
41
|
|
|
29
42
|
const memClass = heavyClass(kind);
|
|
43
|
+
const floor = minFreeMemFor(config, memClass);
|
|
44
|
+
let spentSec = 0;
|
|
45
|
+
if (config.reclaimIdleMinutes && (deps.freeGb ?? defaultFreeGb)() < floor) {
|
|
46
|
+
(deps.reclaim ?? runReclaim)(dir, env);
|
|
47
|
+
spentSec = RECLAIM_TIMEOUT_MS / 1000;
|
|
48
|
+
}
|
|
30
49
|
const result = (deps.acquire ?? acquireHeavy)({
|
|
31
50
|
dir: heavyDir(env),
|
|
32
51
|
slots: config.slots,
|
|
@@ -35,8 +54,8 @@ export async function run(payload, deps = {}) {
|
|
|
35
54
|
node: null,
|
|
36
55
|
cwd: dir,
|
|
37
56
|
kind: memClass,
|
|
38
|
-
minFreeMemGb:
|
|
39
|
-
waitTimeoutMs: Math.min(config.waitTimeoutSec, HOOK_BUDGET_SEC) * 1000,
|
|
57
|
+
minFreeMemGb: floor,
|
|
58
|
+
waitTimeoutMs: Math.max(0, Math.min(config.waitTimeoutSec, HOOK_BUDGET_SEC) - spentSec) * 1000,
|
|
40
59
|
maxHoldMs: config.maxHoldMinutes * 60_000,
|
|
41
60
|
lease: { id: payload.tool_use_id, ttlMs: leaseTtlMs(input, env) },
|
|
42
61
|
env,
|