@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.
Files changed (86) hide show
  1. package/README.md +3 -7
  2. package/bin/morph-spec.js +10 -41
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -4
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/MORPH.md +1 -1
  7. package/framework/agents.json +1 -2
  8. package/framework/commands/morph-archive.md +1 -1
  9. package/framework/commands/morph-preflight.md +3 -4
  10. package/framework/docs/CLI.md +45 -22
  11. package/framework/evals/review-checklist.md +5 -5
  12. package/framework/hooks/README.md +2 -2
  13. package/framework/hooks/claude-code/core/heavy-gate.js +21 -2
  14. package/framework/hooks/claude-code/core/validate-completion.js +0 -35
  15. package/framework/hooks/shared/e2e-check.js +44 -7
  16. package/framework/hooks/shared/heavy-command.js +12 -1
  17. package/framework/hooks/shared/heavy-config.js +12 -0
  18. package/framework/hooks/shared/host-services.js +9 -6
  19. package/framework/hooks/shared/next-step.js +12 -8
  20. package/framework/phases.json +4 -4
  21. package/framework/rules/plan-grammar.md +4 -1
  22. package/framework/schemas/config.schema.json +59 -1
  23. package/framework/schemas/feature.schema.json +15 -0
  24. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  25. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  26. package/framework/skills/core/morph-apply/SKILL.md +8 -5
  27. package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
  28. package/framework/skills/core/morph-apply/references/machine.md +3 -2
  29. package/framework/skills/core/morph-apply/references/review.md +9 -7
  30. package/framework/skills/core/morph-plan/SKILL.md +7 -6
  31. package/framework/skills/core/morph-plan/references/critique.md +1 -1
  32. package/framework/skills/core/morph-uiux/SKILL.md +2 -2
  33. package/framework/templates/IDE-SNIPPETS.md +262 -262
  34. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  35. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  36. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  37. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  38. package/framework/templates/docs/plan.md +2 -1
  39. package/framework/templates/examples/design-system-examples.md +0 -3
  40. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  41. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  42. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  43. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  44. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  45. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  46. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  47. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  48. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  49. package/framework/templates/state.template.json +17 -17
  50. package/framework/templates/ui/design-system.css +226 -226
  51. package/package.json +1 -1
  52. package/src/commands/project/doctor.js +0 -1
  53. package/src/commands/project/heavy.js +13 -0
  54. package/src/commands/state/approve.js +9 -109
  55. package/src/commands/state/report-check.js +44 -2
  56. package/src/commands/validation/verify.js +57 -7
  57. package/src/core/state/state-manager.js +0 -24
  58. package/src/dashboard/app/main.jsx +13 -13
  59. package/src/dashboard/vite.config.js +24 -24
  60. package/src/lib/heavy/cleanup.js +8 -2
  61. package/src/lib/heavy/gate.js +24 -1
  62. package/src/lib/heavy/reclaim.js +107 -0
  63. package/src/lib/heavy/watchdog.js +46 -0
  64. package/src/lib/next-step.js +1 -0
  65. package/src/lib/plan/checks.js +1 -1
  66. package/src/lib/plan/critique.js +23 -3
  67. package/src/lib/procs/prune.js +23 -1
  68. package/src/lib/review-confirmation.js +67 -0
  69. package/src/lib/review-reports.js +1 -0
  70. package/src/lib/risk-detector.js +2 -2
  71. package/src/lib/tasks/test-nodes.js +10 -0
  72. package/src/lib/tasks/test-runner.js +37 -43
  73. package/src/lib/telemetry/telemetry-reader.js +1 -2
  74. package/src/lib/telemetry/verify-event.js +7 -0
  75. package/src/lib/validators/verify-cache.js +63 -0
  76. package/src/lib/validators/verify-runner.js +131 -26
  77. package/src/lib/verify/affected.js +141 -0
  78. package/src/lib/verify/detached.js +154 -0
  79. package/src/utils/agents-installer.js +2 -2
  80. package/src/utils/hooks-installer.js +0 -20
  81. package/bin/validate.js +0 -212
  82. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  83. package/framework/templates/.idea/morph-templates.xml +0 -92
  84. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  85. package/src/commands/observability/telemetry.js +0 -60
  86. 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.1.1
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 approval-status {feature}
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.1.1 by [Polymorphism Tech](https://polymorphism.tech)*
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')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "10.1.1",
3
+ "version": "10.5.2",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -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.1.1 by Polymorphism Tech*
177
+ *morph-spec v10.5.2 by Polymorphism Tech*
@@ -89,4 +89,4 @@ morph-spec doctor
89
89
 
90
90
  ---
91
91
 
92
- *morph-spec v10.1.1 by Polymorphism Tech*
92
+ *morph-spec v10.5.2 by Polymorphism Tech*
@@ -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 |
@@ -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 inteira já rodou antes 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."
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 approval-status {feature}
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 | `approval-status` | plan (and uiux, when the feature has design) and review 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:** run them only through `morph-spec verify {feature}` — never a bare `dotnet test`/`npm test`, which bypasses the heavy-job queue and leaves no proof the gates read. Any red node blocks deployment.
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
 
@@ -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 o orquestrador repete a chamada.
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 task, `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
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 na task (orçamento do `morph-plan` passo 5), e é lá que o avaliador a lê.
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` de escopo **feature** (nunca no de task: cada task pagaria compilador frio), no `e2e down` e no `doctor --procs --prune`, 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`.
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`, risco provado só por `leitura`, 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`).
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
- > **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
725
- > corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
726
- > relógios que o CLI de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
727
- > encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
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 pediu, ou deixa de fazer o que o contrato pede | trecho que nenhum Cn explica, ou o item do contrato ausente |
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): 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
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 telemetry` |
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 incomplete slice reports (hash-deduped, only re-injects when the warning set changes) |
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: minFreeMemFor(config, memClass),
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,