@polymorphism-tech/morph-spec 10.1.1 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/bin/morph-spec.js +1 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +0 -1
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +3 -4
- package/framework/docs/CLI.md +23 -20
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/next-step.js +2 -2
- package/framework/phases.json +4 -4
- package/framework/schemas/config.schema.json +20 -1
- package/framework/schemas/feature.schema.json +1 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +5 -5
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/review.md +5 -5
- package/framework/skills/core/morph-plan/SKILL.md +4 -4
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +7 -3
- package/src/core/state/state-manager.js +0 -24
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +98 -23
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
package/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.2.0
|
|
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.2.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -35,7 +35,6 @@ const stackSetCommand = lazy('../src/commands/project/stack-set.js', 'stackSetCo
|
|
|
35
35
|
const testsAuditCommand = lazy('../src/commands/validation/tests-audit.js', 'testsAuditCommand');
|
|
36
36
|
|
|
37
37
|
const approveCommand = lazy('../src/commands/state/approve.js', 'approveCommand');
|
|
38
|
-
const approvalStatusCommand = lazy('../src/commands/state/approve.js', 'approvalStatusCommand');
|
|
39
38
|
const unapproveCommand = lazy('../src/commands/state/approve.js', 'unapproveCommand');
|
|
40
39
|
const advanceCommand = lazy('../src/commands/state/advance.js', 'advanceCommand');
|
|
41
40
|
const archiveCommand = lazy('../src/commands/state/archive.js', 'archiveCommand');
|
|
@@ -55,8 +54,6 @@ const graphAffectedCommand = lazy('../src/commands/graph/affected.js', 'graphAff
|
|
|
55
54
|
const graphExplainCommand = lazy('../src/commands/graph/explain.js', 'graphExplainCommand');
|
|
56
55
|
const graphDiffCommand = lazy('../src/commands/graph/diff.js', 'graphDiffCommand');
|
|
57
56
|
|
|
58
|
-
const validateCommand = lazy('./validate.js', 'validateCommand');
|
|
59
|
-
const validateFeatureCommand = lazy('../src/commands/validation/validate-feature.js', 'validateFeatureCommand');
|
|
60
57
|
const verifyCommand = lazy('../src/commands/validation/verify.js', 'verifyCommand');
|
|
61
58
|
const e2eUpCommand = lazy('../src/commands/e2e/e2e.js', 'e2eUpCommand');
|
|
62
59
|
const e2eStatusCommand = lazy('../src/commands/e2e/e2e.js', 'e2eStatusCommand');
|
|
@@ -72,7 +69,6 @@ const contextRefreshCommand = lazy('../src/commands/context/context-refresh.js',
|
|
|
72
69
|
|
|
73
70
|
const dashboardCommand = lazy('../src/commands/dashboard/dashboard.js', 'dashboardCommand');
|
|
74
71
|
|
|
75
|
-
const telemetryCommand = lazy('../src/commands/observability/telemetry.js', 'telemetryCommand');
|
|
76
72
|
const costCommand = lazy('../src/commands/observability/cost.js', 'costCommand');
|
|
77
73
|
|
|
78
74
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -216,12 +212,6 @@ program
|
|
|
216
212
|
.option('--json', 'Output as JSON')
|
|
217
213
|
.action((feature, options) => deleteCommand(feature, options));
|
|
218
214
|
|
|
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
215
|
program
|
|
226
216
|
.command('unapprove <feature> <gate>')
|
|
227
217
|
.description('Revoke a phase gate approval (recovery only)')
|
|
@@ -324,29 +314,6 @@ graphCommand
|
|
|
324
314
|
.option('--json', 'Output as JSON')
|
|
325
315
|
.action((options) => graphDiffCommand(options));
|
|
326
316
|
|
|
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
317
|
program
|
|
351
318
|
.command('verify <feature>')
|
|
352
319
|
.description('Run the deterministic verification chain (build + tests + validators + e2e) — code before the LLM judge')
|
|
@@ -361,6 +328,7 @@ program
|
|
|
361
328
|
.option('--skip-build', 'Skip the build node. The run is partial, so the verification stamp is NOT written (reason "skip-build")')
|
|
362
329
|
.option('--skip-e2e', 'Skip the e2e node (Docker bring-up + Playwright)')
|
|
363
330
|
.option('--fresh-e2e', 'Recreate the e2e stack from scratch: down -v (named volumes included), drop the freshness stamp, rebuild, re-seed auth')
|
|
331
|
+
.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
332
|
.option('--no-persist', 'Do not persist the result to feature.json')
|
|
365
333
|
.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')
|
|
366
334
|
.action((feature, options) =>
|
|
@@ -593,14 +561,6 @@ program
|
|
|
593
561
|
.option('--no-open', 'Don\'t auto-open browser')
|
|
594
562
|
.action(dashboardCommand);
|
|
595
563
|
|
|
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
564
|
program
|
|
605
565
|
.command('cost [feature]')
|
|
606
566
|
.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.2.0 by Polymorphism Tech*
|
package/docs/QUICKSTART.md
CHANGED
package/framework/agents.json
CHANGED
|
@@ -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,7 +315,7 @@ 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
|
|
|
@@ -325,7 +329,7 @@ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
|
|
|
325
329
|
|
|
326
330
|
**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
331
|
|
|
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
|
|
332
|
+
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
333
|
|
|
330
334
|
**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
335
|
|
|
@@ -341,7 +345,7 @@ Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.s
|
|
|
341
345
|
|
|
342
346
|
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
347
|
|
|
344
|
-
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora
|
|
348
|
+
**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
349
|
|
|
346
350
|
### e2e
|
|
347
351
|
|
|
@@ -408,11 +412,11 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
|
|
|
408
412
|
|
|
409
413
|
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
414
|
|
|
411
|
-
**Fim do trabalho pesado:** no fim do `verify`
|
|
415
|
+
**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
416
|
|
|
413
417
|
`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
418
|
|
|
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.
|
|
419
|
+
**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
420
|
|
|
417
421
|
`heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`.
|
|
418
422
|
|
|
@@ -496,7 +500,9 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
496
500
|
|
|
497
501
|
**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
502
|
|
|
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`.
|
|
503
|
+
**`--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).
|
|
504
|
+
|
|
505
|
+
**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
506
|
|
|
501
507
|
### plan-check
|
|
502
508
|
|
|
@@ -545,6 +551,7 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
|
|
|
545
551
|
| `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
|
|
546
552
|
| `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
553
|
| `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 |
|
|
554
|
+
| `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
555
|
| `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
556
|
| `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
|
|
550
557
|
| `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 +698,8 @@ testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou d
|
|
|
691
698
|
**Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
|
|
692
699
|
os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
|
|
693
700
|
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
|
|
701
|
+
`test-build-failed` e a suíte **não roda**. E um `command` `dotnet` com `--no-build` que **não** foi precedido de
|
|
702
|
+
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
703
|
aqui", nunca "a flag está ausente". Sem isso, um projeto de teste que deixou de compilar era executado
|
|
697
704
|
a partir do binário do último build bom e reportava verde. `--no-build` continua legítimo — com
|
|
698
705
|
`build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito para isso. O prebuild é
|
|
@@ -721,10 +728,10 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
|
|
|
721
728
|
| 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
729
|
| 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
730
|
|
|
724
|
-
> **
|
|
725
|
-
>
|
|
726
|
-
> relógios que o
|
|
727
|
-
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms
|
|
731
|
+
> **Nenhum relógio do `verify` é de 600 s.** Os 600 s que existem são a espera da fila
|
|
732
|
+
> (`heavy.waitTimeoutSec`) e o teto do **Bash do harness**, externo ao morph-spec: é ele que corta uma
|
|
733
|
+
> 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
|
|
734
|
+
> encadeado**, `up --build` e health do e2e **180 000 ms**, specs Playwright **300 000 ms** (`e2e.specTimeoutMs`), mais `timeoutMs` por nó,
|
|
728
735
|
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
729
736
|
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
|
|
730
737
|
> `verify {feature}` com `run_in_background` e
|
|
@@ -828,10 +835,6 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
828
835
|
|
|
829
836
|
## Observabilidade
|
|
830
837
|
|
|
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
838
|
### cost
|
|
836
839
|
|
|
837
840
|
**`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 +869,6 @@ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit cod
|
|
|
866
869
|
| `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
|
|
867
870
|
| `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
|
|
868
871
|
| `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}` |
|
|
872
|
+
| `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
873
|
|
|
871
874
|
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 @@ 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
|
|
|
@@ -20,10 +20,6 @@ function auditReport(featurePath) {
|
|
|
20
20
|
: [];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
24
|
-
|
|
25
|
-
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
26
|
-
|
|
27
23
|
export async function run(payload) {
|
|
28
24
|
try {
|
|
29
25
|
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
|
|
@@ -42,37 +38,6 @@ export async function run(payload) {
|
|
|
42
38
|
|
|
43
39
|
const currentPhase = feature.phase || derivePhaseForFeature(name, cwd);
|
|
44
40
|
|
|
45
|
-
const watchPath = join(memDir, 'phase-watch.json');
|
|
46
|
-
let phaseWatch = {};
|
|
47
|
-
try {
|
|
48
|
-
if (existsSync(watchPath)) phaseWatch = JSON.parse(readFileSync(watchPath, 'utf-8'));
|
|
49
|
-
} catch { /* corrupt file — start fresh */ }
|
|
50
|
-
|
|
51
|
-
const lastPhase = phaseWatch[name];
|
|
52
|
-
|
|
53
|
-
// Always stored, so the alert fires once per transition.
|
|
54
|
-
try {
|
|
55
|
-
phaseWatch[name] = currentPhase;
|
|
56
|
-
writeFileSync(watchPath, JSON.stringify(phaseWatch, null, 2), 'utf-8');
|
|
57
|
-
} catch {}
|
|
58
|
-
|
|
59
|
-
const isPhaseTransition = lastPhase && lastPhase !== currentPhase;
|
|
60
|
-
const enteringImplement = COMPACT_TRIGGER_PHASES.has(currentPhase);
|
|
61
|
-
const comingFromHeavyPhase = HEAVY_SOURCE_PHASES.has(lastPhase);
|
|
62
|
-
|
|
63
|
-
if (isPhaseTransition && enteringImplement && comingFromHeavyPhase) {
|
|
64
|
-
warnings.push(
|
|
65
|
-
`⚡ Phase advanced: ${lastPhase} → implement`,
|
|
66
|
-
``,
|
|
67
|
-
`💡 Recommended before starting implementation:`,
|
|
68
|
-
` Run /compact to free up context — plan.md and the design`,
|
|
69
|
-
` loaded during planning take significant space. A compact now gives`,
|
|
70
|
-
` Claude a clean slate focused on writing code.`,
|
|
71
|
-
``,
|
|
72
|
-
` After /compact, resume with: /morph-apply ${name}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
41
|
// Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
|
|
77
42
|
try {
|
|
78
43
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
@@ -187,23 +187,40 @@ export function decideIsolation({ findings = [], insideWorktree = false } = {})
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// HEAD alone would let a stack built before an uncommitted edit read as fresh: status and diff
|
|
190
|
-
// make any edit move the stamp.
|
|
190
|
+
// make any edit move the stamp. `.morph/` stays out, config.json aside: the verify's own commits
|
|
191
|
+
// must not rebuild a stack whose source did not change. `null` outside a git repo.
|
|
191
192
|
export function computeSourceStamp(cwd, exec = defaultExec) {
|
|
192
193
|
const head = exec('git', ['rev-parse', 'HEAD'], { cwd });
|
|
193
194
|
if (head.code !== 0) return null;
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
196
|
+
const tree = exec('git', ['ls-tree', 'HEAD'], { cwd });
|
|
197
|
+
const status = exec('git', ['status', '--porcelain', ...OUTSIDE_MORPH], { cwd });
|
|
198
|
+
const diff = exec('git', ['diff', 'HEAD', ...OUTSIDE_MORPH], { cwd });
|
|
199
|
+
const entries = tree.code === 0
|
|
200
|
+
? tree.output.split('\n').filter((line) => line.split('\t')[1] !== '.morph').join('\n')
|
|
201
|
+
: head.output.trim();
|
|
197
202
|
|
|
198
203
|
return createHash('sha1')
|
|
199
|
-
.update(
|
|
204
|
+
.update(entries)
|
|
200
205
|
.update('\0')
|
|
201
206
|
.update(status.code === 0 ? status.output : '')
|
|
202
207
|
.update('\0')
|
|
203
208
|
.update(diff.code === 0 ? diff.output : '')
|
|
209
|
+
.update('\0')
|
|
210
|
+
.update(readConfigText(cwd))
|
|
204
211
|
.digest('hex');
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
const OUTSIDE_MORPH = ['--', '.', ':(exclude).morph'];
|
|
215
|
+
|
|
216
|
+
function readConfigText(cwd) {
|
|
217
|
+
try {
|
|
218
|
+
return readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8');
|
|
219
|
+
} catch {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
207
224
|
export function stampPath(gitCommonDir, projectName) {
|
|
208
225
|
return join(gitCommonDir, 'morph-spec', `e2e-stamp-${projectName}.json`);
|
|
209
226
|
}
|
|
@@ -775,6 +792,11 @@ export function bringUpE2e(
|
|
|
775
792
|
return result({ status: 'pass', stage: null, reason: stack.reason, hostServices: host, auth });
|
|
776
793
|
}
|
|
777
794
|
|
|
795
|
+
function specTimeoutFor(config) {
|
|
796
|
+
const configured = config?.e2e?.specTimeoutMs;
|
|
797
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_SPEC_TIMEOUT;
|
|
798
|
+
}
|
|
799
|
+
|
|
778
800
|
/** @returns {{ status: 'pass'|'fail'|'skip', command: string|null, output: string, reason: string|null, warnings: string[] }} */
|
|
779
801
|
export function runE2eCheck(cwd, options = {}) {
|
|
780
802
|
const {
|
|
@@ -787,7 +809,7 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
787
809
|
forceFresh = false,
|
|
788
810
|
insideWorktree = isSecondaryWorktree(cwd),
|
|
789
811
|
healthTimeout = DEFAULT_HEALTH_TIMEOUT,
|
|
790
|
-
specTimeout =
|
|
812
|
+
specTimeout = specTimeoutFor(config),
|
|
791
813
|
beforeHeavy = null,
|
|
792
814
|
countSpecs = null,
|
|
793
815
|
_exec: exec = defaultExec,
|
|
@@ -858,6 +880,12 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
858
880
|
}
|
|
859
881
|
}
|
|
860
882
|
|
|
883
|
+
// Playwright's list reporter names a failure as ` 1) [chromium] › e2e/a.spec.ts:3:5 › title`.
|
|
884
|
+
function failedSpecNames(output) {
|
|
885
|
+
const names = [...String(output).matchAll(/^\s+\d+\)\s+(.+›.+)$/gm)].map((match) => match[1].trim());
|
|
886
|
+
return [...new Set(names)];
|
|
887
|
+
}
|
|
888
|
+
|
|
861
889
|
// The suite gets the stack's env (with PLAYWRIGHT_BASE_URL): without it the config hits a hardcoded
|
|
862
890
|
// localhost:3000 while the app sits on the feature's port, an ECONNREFUSED that looks like a broken app.
|
|
863
891
|
function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec) {
|
|
@@ -885,18 +913,27 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv,
|
|
|
885
913
|
// Counted from the FULL output, before the tail cut; unparseable is null, never 0.
|
|
886
914
|
const count = typeof countSpecs === 'function' ? (countSpecs(run.output) ?? null) : null;
|
|
887
915
|
|
|
888
|
-
|
|
916
|
+
// One rerun of the failed specs only: a flake must not cost a whole verify. `--last-failed`
|
|
917
|
+
// needs Playwright 1.44+; an older one exits non-zero, and the node stays red.
|
|
918
|
+
const retry = run.code !== 0 && spec.args.includes('playwright')
|
|
919
|
+
? exec(spec.command, [...spec.args, '--last-failed'], { cwd: spec.cwd, timeout: specTimeout, env: composeEnv })
|
|
920
|
+
: null;
|
|
921
|
+
|
|
922
|
+
if (run.code !== 0 && retry?.code !== 0) {
|
|
889
923
|
return {
|
|
890
924
|
status: 'fail',
|
|
891
925
|
command: commandLine,
|
|
892
|
-
output: tailLines(run.output, OUTPUT_TAIL_LINES),
|
|
926
|
+
output: tailLines(retry?.output ?? run.output, OUTPUT_TAIL_LINES),
|
|
893
927
|
reason: 'e2e specs failed against the running stack',
|
|
894
928
|
count,
|
|
895
929
|
};
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
const flakySpecs = retry ? failedSpecNames(run.output) : [];
|
|
933
|
+
|
|
898
934
|
return {
|
|
899
935
|
status: 'pass',
|
|
936
|
+
flakySpecs,
|
|
900
937
|
command: commandLine,
|
|
901
938
|
output: '',
|
|
902
939
|
reason: notes.length ? notes.join('; ') : null,
|
|
@@ -57,6 +57,14 @@ function firstArg(args, valueFlags = new Set()) {
|
|
|
57
57
|
return undefined;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const POWERSHELL_HOSTS = new Set(['powershell', 'pwsh']);
|
|
61
|
+
const TEST_SCRIPT_SUFFIX = 'test-script';
|
|
62
|
+
|
|
63
|
+
function runsTestScript(args) {
|
|
64
|
+
const script = args.find((a) => /\.ps1$/i.test(a));
|
|
65
|
+
return Boolean(script) && /test/i.test(script.replace(/^.*[\\/]/, ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function kindOfTokens(tokens) {
|
|
61
69
|
const [head, ...args] = tokens;
|
|
62
70
|
if (!head || args.some((a) => INFO_FLAGS.has(a))) return null;
|
|
@@ -64,6 +72,7 @@ function kindOfTokens(tokens) {
|
|
|
64
72
|
const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
|
|
65
73
|
: PACKAGE_MANAGERS.has(exe) ? PM_DIR_FLAGS : undefined);
|
|
66
74
|
|
|
75
|
+
if (POWERSHELL_HOSTS.has(exe)) return runsTestScript(args) ? `${exe} ${TEST_SCRIPT_SUFFIX}` : null;
|
|
67
76
|
if (exe === 'dotnet') return DOTNET_HEAVY.has(sub) ? `dotnet ${sub}` : null;
|
|
68
77
|
if (exe === 'msbuild') return 'msbuild';
|
|
69
78
|
if (exe === 'vitest' || exe === 'jest') return exe;
|
|
@@ -115,7 +124,7 @@ export const REVIEW_AGENT_TYPES = new Set(['morph-verifier', 'morph-reviewer', '
|
|
|
115
124
|
export const QUEUE_FULL_EXIT = 3;
|
|
116
125
|
|
|
117
126
|
const LIST_FLAGS = new Set(['--list', '--listTests', '--list-tests']);
|
|
118
|
-
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?))$/;
|
|
127
|
+
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?)|(powershell|pwsh) test-script)$/;
|
|
119
128
|
const NODE_RUNNERS = new Set(['vitest', 'jest']);
|
|
120
129
|
|
|
121
130
|
function morphArgs(tokens) {
|
|
@@ -158,6 +167,8 @@ const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
|
|
|
158
167
|
const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
|
|
159
168
|
|
|
160
169
|
function isFilteredRun(kind, tokens) {
|
|
170
|
+
// A script's arguments are its own contract: no flag here proves it narrowed the run.
|
|
171
|
+
if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return false;
|
|
161
172
|
const runnerAt = executable(tokens[0]) === 'node'
|
|
162
173
|
? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
|
|
163
174
|
: 0;
|
|
@@ -33,7 +33,7 @@ const HOW = {
|
|
|
33
33
|
`\`morph-spec verify ${f} --json\` sem filtro; passou de 10 min, run_in_background.`,
|
|
34
34
|
],
|
|
35
35
|
fix: (f) => [
|
|
36
|
-
`Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro.`,
|
|
36
|
+
`Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro: ele começa pelas suítes vermelhas e só segue se passarem.`,
|
|
37
37
|
],
|
|
38
38
|
escalate: () => [
|
|
39
39
|
'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
|
|
@@ -42,7 +42,7 @@ const HOW = {
|
|
|
42
42
|
'Siga .claude/skills/morph-apply/references/review.md.',
|
|
43
43
|
],
|
|
44
44
|
finish: (f) => [
|
|
45
|
-
|
|
45
|
+
`\`morph-spec context refresh\`, commit, e \`morph-spec finish ${f} --pr\` (\`--merge\` com \`finish.mode: "merge"\` no config), sem perguntar.`,
|
|
46
46
|
],
|
|
47
47
|
status: (f) => [
|
|
48
48
|
`Rode \`morph-spec status ${f}\` e confira a fase em .morph/features/${f}/feature.json.`,
|