@polymorphism-tech/morph-spec 8.43.0 → 8.45.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/CLAUDE.md +16 -13
- package/README.md +10 -10
- package/bin/morph-spec.js +10 -13
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -4
- package/docs/QUICKSTART.md +6 -6
- package/framework/CLAUDE.md +6 -8
- package/framework/MORPH.md +39 -45
- package/framework/agents.json +8 -9
- package/framework/commands/morph-apply.md +1 -3
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/docs/CLI.md +22 -17
- package/framework/evals/review-checklist.md +90 -0
- package/framework/evals/trace-schema.md +2 -12
- package/framework/hooks/README.md +2 -3
- package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
- package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
- package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
- package/framework/hooks/claude-code/core/validate-completion.js +3 -56
- package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/build-check.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +1 -1
- package/framework/hooks/shared/phase-utils.js +0 -5
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/phases.json +4 -6
- package/framework/rubrics/backend.json +1 -1
- package/framework/rubrics/default.json +1 -1
- package/framework/rubrics/frontend.json +1 -1
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/rules/morph-workflow.md +6 -6
- package/framework/schemas/feature.schema.json +2 -28
- package/framework/schemas/tasks.schema.json +6 -2
- package/framework/skills/README.md +1 -2
- 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 +145 -246
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
- package/framework/skills/core/morph-implement/SKILL.md +101 -201
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +171 -287
- package/framework/skills/core/morph-proposal/SKILL.md +4 -5
- package/framework/skills/core/morph-review/SKILL.md +109 -430
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +2 -2
- package/framework/skills/core/morph-uiux/SKILL.md +2 -3
- package/framework/standards/STANDARDS.json +54 -10
- package/framework/standards/ai-agents/agent-archetypes.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/agent-spec.md +1 -1
- package/framework/standards/ai-agents/batching.md +1 -1
- package/framework/standards/ai-agents/context-providers.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
- package/framework/standards/ai-agents/cost-and-budget.md +1 -1
- package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/mcp-server.md +1 -1
- package/framework/standards/ai-agents/mcp-tools.md +1 -1
- package/framework/standards/ai-agents/media-pipeline.md +1 -1
- package/framework/standards/ai-agents/media-video.md +1 -1
- package/framework/standards/ai-agents/middleware-patterns.md +1 -1
- package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
- package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
- package/framework/standards/ai-agents/modalities-vision.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
- package/framework/standards/ai-agents/observability-patterns.md +3 -3
- package/framework/standards/ai-agents/production.md +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +1 -1
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
- package/framework/standards/ai-agents/service-tier-flex.md +1 -1
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/ai-agents/testing-ai.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +226 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/README.md +1 -1
- package/framework/templates/REGISTRY.json +0 -81
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/spec.md +19 -205
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +2 -2
- package/src/commands/e2e/e2e.js +1 -1
- package/src/commands/project/heavy.js +1 -1
- package/src/commands/project/status.js +3 -3
- package/src/commands/project/worktree.js +34 -16
- package/src/commands/state/approve.js +9 -50
- package/src/commands/state/finish.js +4 -0
- package/src/commands/state/gate-check.js +18 -107
- package/src/commands/state/gate-decision.js +1 -1
- package/src/commands/state/report-check.js +42 -17
- package/src/commands/validation/mutate.js +1 -1
- package/src/commands/validation/scout-check.js +28 -0
- package/src/commands/validation/validate-feature.js +5 -32
- package/src/commands/validation/verify.js +16 -25
- package/src/core/paths/output-schema.js +0 -2
- package/src/core/state/state-manager.js +1 -2
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/lib/format.js +5 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Rubrics.jsx +7 -11
- package/src/dashboard/scanner/docs-scanner.js +1 -2
- package/src/dashboard/scanner/features-scanner.js +3 -4
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/output-collector.js +0 -1
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/e2e/compose-env.js +5 -2
- package/src/lib/evaluator-report.js +52 -334
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/handoff-generator.js +4 -5
- package/src/lib/git/evidence-staleness.js +39 -0
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/mutation/manifest.js +8 -2
- package/src/lib/mutation/report.js +5 -3
- package/src/lib/mutation/runner.js +9 -7
- package/src/lib/risk-detector.js +63 -125
- package/src/lib/scout/scout-check.js +293 -0
- package/src/lib/tasks/eval-node.js +4 -34
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +7 -5
- package/src/lib/tasks/test-nodes.js +1 -1
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/reuse-validator.js +6 -8
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/lib/worktree/env-allocator.js +35 -5
- package/src/scripts/setup-infra.js +1 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +5 -9
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +3 -10
- package/src/utils/skills-installer.js +1 -1
- package/framework/evals/proof-rubrics/README.md +0 -76
- package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
- package/framework/evals/proof-rubrics/fiacao.md +0 -54
- package/framework/evals/proof-rubrics/guard-cego.md +0 -52
- package/framework/evals/proof-rubrics/media-claims.md +0 -109
- package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
- package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
- package/framework/evals/rubrics/README.md +0 -45
- package/framework/evals/rubrics/architecture.md +0 -103
- package/framework/evals/rubrics/code-quality.md +0 -98
- package/framework/evals/rubrics/condensed.md +0 -57
- package/framework/evals/rubrics/contracts.md +0 -67
- package/framework/evals/rubrics/test-coverage.md +0 -99
- package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
- package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
- package/framework/evals/suites/standard.yaml +0 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
- package/framework/skills/core/morph-eval/SKILL.md +0 -64
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/framework/templates/docs/clarifications.md +0 -253
- package/framework/templates/docs/onboarding.md +0 -122
- package/framework/templates/docs/schema-analysis.md +0 -119
- package/framework/templates/docs/user-stories.md +0 -34
- package/src/commands/state/score.js +0 -147
package/framework/docs/CLI.md
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando). **No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
|
|
10
10
|
- **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta). Ao encerrar a **uiux**, a assinatura `auto` é recusada (exit 1) pelo mesmo predicado do `approve` e do `gate-check uiux`: checklist de tela incompleto no `1-design/mockups.md` — toda `## Tela:` com o `### Checklist de tela` respondido item a item (`src/lib/specs/screen-checklist.js`), com a `Superfície` sendo uma das `surfaces` da `ui-designer` no `agents.json` instalado. Destravam a decisão gravada com `pause: false` (`gate-decision`, e só para o mockup que ela viu) e a assinatura humana (`--approver <nome>`); `--force` não dispensa o checklist, como no `approve`. Quem avança sobre o checklist incompleto com `--approver` vê o que ficou faltando. Sem `mockups.md`, ou com o mockup no formato antigo (telas `## S001:`, sem `## Tela:`), não há o que cobrar.
|
|
11
|
-
- **`morph-spec
|
|
12
|
-
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (rubrica `evals/rubrics/contracts.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
11
|
+
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations` — qualquer outra chave é ignorada com aviso. No **review**, o `gate-check` lê o veredito do `evaluator-report.md` (`evaluatorReportState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.evaluatorVerdict = {status, verdict, blockers, untyped, superseded}`: `**Verdict:** SHIP` sem bloqueador aberto passa; `FIX`, bloqueador aberto, bloqueador sem tipo (`uc`/`teste`/`regressao`/`seguranca`) ou relatório sem `**Verdict:**` **pausa o Gate 3**. O relatório é append-only e vale o ÚLTIMO `**Verdict:**`; vereditos anteriores aparecem em `superseded`. `evaluatorVerdict` não é sobrescrevível por `--signals`. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (`framework/evals/review-checklist.md`, "Feature que gera mídia"). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
13
12
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
14
|
-
- **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`
|
|
13
|
+
- **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
|
|
15
14
|
|
|
16
15
|
## Features
|
|
17
16
|
|
|
@@ -22,16 +21,16 @@
|
|
|
22
21
|
|
|
23
22
|
## Worktree / Fleet
|
|
24
23
|
|
|
25
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--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). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
|
|
24
|
+
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--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). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação. **Toda saída de sucesso carimba `memoryNote`**: a frase que nomeia o que o fechamento NÃO deixa registrado no repositório — decisão do usuário, pendência a observar em produção, custo medido. O commit de archive, o `decisions.md`, os use cases promovidos e o `git log` já guardam o resto, e memória de sessão escrita por cima desses fatos nasce redundante e envelhece sem revisão. Recusa não a carimba: não há fechamento sobre o qual gravar nada.
|
|
26
25
|
- **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
|
|
27
26
|
- **`morph-spec usecase check <feature> [--json]`** — o ciclo de vida do acervo contra a feature, rodável antes de existir `tasks.json` (o `spec.md` congela no Gate 2, então é aqui que uma linha de revogação errada ainda tem conserto): task cujo `usecase` está `superseded` (no acervo, na cópia da feature ou revogado pelo próprio `spec.md`), frontmatter de supersessão malformado (`superseded` sem `superseded_by` UC-{nn}, `superseded_by`/`scope` sem `status: superseded`) e linha de revogação que o `finish` não aplicaria (id malformado, UC fora do acervo, substituto inexistente, ADR fora do índice). Avisos, exit 0. Com `tasks.json`, roda também o cruzamento task × use case do `dag` — e sai 1 onde o `dag` sairia (UC inexistente, schema inválido). `--json`: `{feature, useCases, lifecycle: {warnings}}`.
|
|
28
27
|
- **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
|
|
29
28
|
- **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). `remove` recusa, antes de tocar em qualquer junction, quando o cwd do processo está no alvo (`cwd-inside-worktree`) **ou quando a sessão do Claude Code que o rodou está ancorada nele** (`session-anchored-in-worktree` — mesmo sinal do `finish`, acima); a recusa é o adiamento: feche a sessão e rode de uma aberta na raiz. Falha de remoção reporta a CAUSA real: um diretório travado vence o eco `is not a working tree` das tentativas seguintes, e `alreadyDeregistered` avisa quando o git já desregistrou a árvore e só o diretório vazio sobrou. **Fechar a feature** continua sendo `morph-spec finish` (acima).
|
|
30
29
|
**Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
|
|
31
30
|
**Base da árvore de task.** Com `worktree.baseRef` ausente nas settings e o cwd de quem despacha numa branch `morph/{f}`, o hook passa `--base morph/{f}`: o sub-agent do apply nasce com o trabalho da feature (nada de `git merge --ff-only morph/{f}` como passo 0). `baseRef: "head"` usa o que o cwd tiver checado; `baseRef: "fresh"` explícito força a branch default. Com base `morph/{f}`, o `worktree.env` grava `MORPH_FEATURE={f}` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
|
|
32
|
-
**Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
|
|
31
|
+
**Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. **Um projeto cujo compose lê a porta por outro nome** (`${PG_PORT}` em vez de `POSTGRES_PORT`) declara `e2e.portEnvNames` no `config.json` (`{ "postgres": "PG_PORT" }`; chaves `web`, `api`, `postgres`, `redis`): o nome declarado sai com o **mesmo valor** em todo lugar do layout e, sendo layout, o `envFile` de credenciais não o sobrescreve (vale o mesmo aviso em stderr). Entrada que não dá para honrar (porta desconhecida, nome inválido ou reservado, nome repetido) é descartada e nomeada em stderr. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
|
|
33
32
|
- **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
|
|
34
|
-
- **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
|
|
33
|
+
- **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido. **Nada é commitado na branch principal por causa do esqueleto da feature na raiz:** o registro `worktree` é apagado do `feature.json`, e só um registro que já estava **rastreado** vira commit. O esqueleto não rastreado de uma feature já arquivada na raiz é apagado com backup (`shedBackup`); o de uma feature sem archive fica como está.
|
|
35
34
|
- **`morph-spec worktree reap [--dry-run] [--grace <min>] [--json]`** — remove as **sobras de sub-agent**: o worktree `agent-<hex>` (branch `morph-task/agent-<hex>`, o que o `Agent` com `isolation: "worktree"` deixa para trás) e a branch órfã `morph-task/agent-<hex>`. **Só remove se TODAS as provas fecham; na dúvida preserva e lista com a razão:** (1) é de agent — diretório `agent-<hex>` sob `worktrees/` e branch `morph-task/agent-<hex>` (ou HEAD destacado); `morph-task/<nome>`, features e `claude -w` nunca entram; (2) o agente **parou** — o último evento do `.morph/logs/events.jsonl` da árvore é o `AgentUsage` dele (`agentId` = o `<hex>`); sem esse evento só lista (`no-usage-event`); (3) **graça** — o log parou de crescer há ≥ `worktree.reap.graceMinutes` (30; `--grace`), porque um agente parado ainda pode ser retomado por `SendMessage`; (4) **árvore limpa** — `git status --porcelain --untracked-files=all`; arquivo não rastreado também bloqueia (`dirty`); (5) **sem commit exclusivo** — `git rev-list <tip> --not <refs>`, com `refs` = branches locais, remotas e tags **menos todas as `morph-task/agent-*`**. Nunca `--all`: ele conta o HEAD de cada worktree e o da própria árvore protegeria o commit; e dois agentes no mesmo commit exclusivo se protegeriam um ao outro — assim preservam os dois; (6) não é o cwd nem a âncora da sessão. Medir não deu (`null`) sempre preserva. **Como remove:** pelo núcleo do `worktree remove --target --if-clean` (desmonta as junctions antes, backup do que só existe ali em `finish-backup/task/`, retenção por namespace), nunca `rm -rf`; **falha de remoção devolve as junctions** (o `worktree remove` manual as deixa desmontadas de propósito): o agente pode estar vivo, e sem `.claude` os hooks dele morrem. Depois apaga a branch (`git branch -D`, com a contagem de commits exclusivos refeita antes). **Um lock por repositório** (`<git-common-dir>/morph-spec/agent-reap.lock`) serializa dois reapers: sem ele, o relink de um recriaria junctions no meio da remoção do outro, e o `--force` as atravessaria; lock de pid vivo → `busy: true` e nada é feito. Branch órfã sai pela mesma regra 5, com a graça medida pelo reflog da branch (o `provision` cria a branch antes do worktree). Idempotente: a 2ª rodada não acha nada. **Quem dispara:** as skills (`morph-apply` §3b-w passo 4, `morph-implement` pós-dispatch, `morph-review` depois de ler o `evaluator-report`), o SessionStart (`worktree-context` solta um worker destacado, só na raiz, no máximo 1 a cada 10 min e só se houver `agent-<hex>` no disco; `MORPH_WORKTREE_REAP=0` desliga) e o `doctor --worktrees --prune`. `--json`: `{ ok, dryRun, graceMinutes, reaped[], branchesDeleted[], wouldReap[] (só no dry-run), preserved[{path,branch,hex,reason,detail}], active, busy? }`. **Telemetria:** remover a árvore leva o `events.jsonl` dela (o custo do agent em `cost --all-trees`); o conteúdo de `.morph/` fica no backup `finish-backup/task/`, como já ficava no `worktree remove --target` do `morph-apply`.
|
|
36
35
|
- **`.vscode/settings.json` (editor)** — `init`, `update`, `worktree setup|provision` e `doctor --worktrees --prune` adicionam a `files.watcherExclude`, `search.exclude` e `files.exclude` os globs `**/worktrees/**` (e o legado `**/.worktrees/**`) e `**/node_modules/**`; `**/bin/**` e `**/obj/**` entram em `watcherExclude` e `search.exclude` sempre e em `files.exclude` só em projeto .NET (stack `dotnet` na config, ou `.sln`/`.slnx`/`.csproj` na raiz), porque num projeto JS o `bin/` guarda código-fonte. **Só adiciona o que falta:** chave do usuário, ordem e valor existente ficam (`"**/bin/**": false` continua `false`), e a 2ª rodada não muda um byte. **JSONC:** não há escritor que preserve comentário, então arquivo **com comentário, com vírgula sobrando, ilegível, com raiz que não é objeto, ou rastreado pelo git** não é escrito — o relatório (`editorSettings` no `--json` de `setup`/`provision`, `prune.editorSettings` no do `doctor`) traz `status: "suggest"`, a `reason` e o `snippet` só com o que falta, para colar. Legível e sem comentário, reescreve preservando BOM, EOL e indentação. **O arquivo criado** entra no `info/exclude` do common dir (`git-exclude.js`): não rastreado, contaria como sujeira da raiz, e o `finish --merge` recusa raiz suja. Desliga: `worktree.editorSettings: false` na config, ou `MORPH_EDITOR_SETTINGS=0`. O `update --dry-run` prevê a escrita (`writeFile`). **O que NÃO cobre:** as chaves cortam watcher, busca e explorer — não governam a descoberta do Git nem o language server. `git.repositoryScanMaxDepth` (padrão 1) não alcança `worktrees/<n>` (profundidade 2), e `git.detectWorktrees` vem **desligado** por padrão (`false`, `scope: resource`; lido no `package.json` da extensão Git do VS Code 1.124.2) e é um recurso de LISTAGEM: mostra as worktrees em *Source Control Repositories*, até `git.detectWorktreesLimit` (50). Ligá-lo faz o VS Code enxergar mais repositórios, não menos, e a escrita no workspace sobrepõe uma preferência global do usuário — por isso o morph-spec não o grava; quem o quer liga na própria config. Não medido: se o C# Dev Kit respeita `files.exclude`, e o antes/depois de RAM e de processos do VS Code com as exclusões.
|
|
37
36
|
- **`worktree.root` (worktrees fora da árvore aberta)** — chave de `.morph/config/config.json` (ou `MORPH_WORKTREE_ROOT`, por rodada): absoluta, `~/…` ou relativa à raiz primária. As worktrees **novas** (`setup`, `provision`, o hook `WorktreeCreate` e o fallback dele em git puro) nascem em `<root>/<repo>-<hash>/<nome>`; o hash é da raiz do repositório, para dois repositórios de mesmo nome dividirem o mesmo root sem colidir. **Sem a chave, nada muda.** **Só aceita root FORA da árvore:** um valor que resolve para dentro da raiz do projeto é **ignorado com aviso** (`worktreeRootWarning` no `--json` de `setup`/`provision`) e valem `worktrees/`, porque toda lista orientada por esse nome (`.gitignore`, `graphifyignore`, descoberta de testes, o `tar` do `--linux`, os globs do editor) deixaria de enxergá-lo em silêncio. Um root **acima** da raiz (`..`) é aceito: o contêiner vira um irmão do repositório. **"Está no contêiner" é a UNIÃO** do layout configurado com `worktrees/` e `.worktrees/`: a árvore que já existe continua reconhecida depois que a chave é ligada ou trocada, e uma criada em qualquer outro lugar segue acusada como `out-of-container`. O resolvedor é um só (`framework/hooks/shared/worktree-root.js`; hook não importa `src/`) e é lido por `buildWorktreePath`, pelo mkdir do pai e por `findWorktreeForFeature`; pelo `doctor --worktrees` (`inContainer` e a varredura de resíduo, que olha só `<root>/<repo>-<hash>/` — nunca a pasta de outro repositório, senão o `--prune` apagaria pastas alheias); pelo reaper; pelo SessionStart (`isOutsideContainer`); pelo registro de projetos (a worktree é detectada pelo `.git` ser um arquivo que aponta para `.git/worktrees/`, não pelo nome do diretório pai); e pelos scanners do dashboard. `finish`, `remove` e `list` andam pelo `git worktree list` e não dependem do lugar. **Não faz:** trocar a chave não move as worktrees que já existem.
|
|
@@ -45,9 +44,9 @@
|
|
|
45
44
|
## Validação
|
|
46
45
|
|
|
47
46
|
- **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. UC com nome fora do padrão `UC-{nn}-{slug}.md` (slug com maiúscula ou `_`) continua encontrado, com aviso: o `finish` não o promove ao acervo. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. Os achados de ciclo de vida do `usecase check` (task sobre UC `superseded`, revogação que o `finish` não aplicaria) saem sob `⚠ Ciclo de vida do acervo`, também só aviso, na chave `lifecycle`. **Tamanho de task:** task com mais de 8 `outputs` (`MAX_TASK_OUTPUTS`) é **erro** (exit 1) enquanto o Gate 2 não foi aprovado — fatie por pós-condição — e só aviso depois dele (chore/hotfix nascem com o plano aprovado por política); o `--json` traz `taskSize: {max, oversized, blocking}`. `--mermaid` nunca é bloqueado.
|
|
48
|
-
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. 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"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. 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). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). 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. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`.
|
|
49
|
-
- **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`). Três medidas, cada uma **um aviso, nunca erro**, mapeado em `code_quality` da rubrica: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `T{n}`, `Gate N`, `spec.md
|
|
50
|
-
- **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--no-subset] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** —
|
|
47
|
+
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. 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"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. 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). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). 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. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. A execução que gravou a baseline persiste o stamp; a que não gravou nada, não. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **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). **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.
|
|
48
|
+
- **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`). Três medidas, cada uma **um aviso, nunca erro**, mapeado em `code_quality` da rubrica: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `T{n}`, `Gate N`, `spec.md`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1,5×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário). Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas. 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. Fica fora do modo `light`. 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ê.
|
|
49
|
+
- **`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--no-subset] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o avaliador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um UC exige e o avaliador não achou o teste que a morde. **Manifesto** (`--plan`, default `4-review/mutants.json`): `{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a","expect":"OrdersTests.RecusaSemItens deve falhar"}]}` — `expect` é obrigatório (qual teste deve falhar e por quê, dito antes de rodar), `find` começa na `line` e é único nela; barra invertida do código vai como `{{BS}}` (o transporte do Bash a reduz). Mais de 3 mutantes ou `expect` ausente: exit 2. **Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o avaliador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado; sem subconjunto utilizável, a suíte inteira roda e a morte tem de se repetir. **Reversão:** bytes do snapshot com md5 conferido, por escrita nova (nunca `os.utime`), rebuild forçado e DLL igual à de antes do 1º mutante. **Exit:** `0` rodou (sobrevivente é achado), `1` a árvore já não compilava, `2` uso, `3` semáforo ocupado, `4` **inseguro** (arquivo não restaurado ou binário que não bate). Segura **um** slot `heavy`.
|
|
51
50
|
- **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
|
|
52
51
|
- **`morph-spec heavy status [--json]`** e **`morph-spec heavy run [--label <x>] -- <comando…>`** — semáforo de nós pesados **por usuário**: os slots vivem em `%LOCALAPPDATA%morph-specheavy` (Windows) ou `$XDG_STATE_HOME/morph-spec/heavy` (`~/.local/state/…`), compartilhados por **todas** as sessões da máquina, com `heavy.slots` (2) vagas. **Quem toma slot:** build, testes (inclusive `--linux`) e e2e do `verify` — um slot por `verify`, adquirido só imediatamente antes do primeiro spawn real (nó pulado não adquire e a espera fica fora do `durationMs`) —, o `e2e up` (só durante a subida da stack) e o `heavy run`, que roda qualquer comando sob slot e repassa o exit code (`127` se não conseguiu executá-lo; `npm`/`npx`/`pnpm`/`yarn` viram `.cmd` no Windows). `heavy status` é **somente leitura**: quem segura cada slot (pid, label, feature, nó, idade), a limpeza exclusiva do build-server se houver e a RAM livre. **Espera:** slot cheio, ou RAM livre (`os.freemem()`) abaixo de `heavy.minFreeMemGb` (4), espera até `heavy.waitTimeoutSec` (600) avisando em **stderr** a cada 30 s e nomeando quem segura (o `--json` mantém o stdout limpo). RAM baixa espera **com ou sem** holder de slot: a pressão costuma vir de fora (vmmem do WSL, IDE), e "sem holder → roda" reproduziria o "roda e morre". **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. **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. **Config** (`.morph/config/config.json`, bloco `heavy`, schema em `config.schema.json`): `enabled`, `slots`, `minFreeMemGb`, `waitTimeoutSec`, `maxHoldMinutes`, `shutdownBuildServer`, `disableNodeReuse`, `gateBash`; valor ilegível cai no default **com aviso** — o bloco nunca bloqueia trabalho por estar errado. 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`. **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. Com `heavy.enabled: false` nunca roda: sem semáforo, nada prova que outra sessão não está compilando. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`. `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). **Bash cru (`heavy.gateBash`, opt-in, padrão `false`):** o `dotnet build`/`dotnet test`/`npm test` de um sub-agent 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. `heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`. **Não reescreve o comando:** a doc do Claude Code avalia as regras de permissão contra o comando **reescrito**, e o `allow` que acompanha um `updatedInput` aprovaria o que o prompt perguntaria; por isso não há reescritor (o `allow` não atropela regra `deny`/`ask`, mas a reescrita esconderia o comando delas). **O que a vaga não cobre:** ela não tem processo vivo para vigiar (o hook sai na hora), então vence por **TTL** — o `timeout` da chamada, no máximo o da plataforma, mais 30 s — e uma chamada recusada por regra de permissão ou interrompida pelo usuário, que não dispara `Post*`, segura o slot até lá. O TTL conta desde o `PreToolUse`, que roda **antes** do prompt de permissão: se a aprovação demora mais que o `timeout` + 30 s, a vaga vence com o comando ainda por rodar e outra chamada pode tomá-la (sobreassinatura, do lado seguro). Chamadas de Bash de um mesmo lote paralelo rodam **em série** no Claude Code (medido na 2.1.285: o `Pre` da 2ª só rodou depois do `Post` da 1ª), então um agente não fica esperando uma vaga que só a própria chamada anterior liberaria. `run_in_background`, PowerShell e comando fora da lista não são cobertos, e a cláusula 6 dos templates de dispatch (`heavy run`) segue valendo para eles. **Medido** com `claude -p` real e a chain instalada: fila cheia recusa após a espera e não mexe na vaga alheia; a vaga está presa durante a chamada e volta vazia depois, com exit 0 e com exit 1. Custo num Bash **leve**: ~1,5 ms na chain Pre (mediana 61,4 → 62,9 ms, medido intercalado) e ~0 na Post; os módulos do semáforo só carregam quando o comando é pesado. O núcleo (`heavy-config`, `heavy-holders`, `heavy-slots`) mora em `framework/hooks/shared`, porque hook não importa `src/`.
|
|
53
52
|
- **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias. Uma linha `.git/morph-spec (<tamanho> · N backup(s) do finish)` mostra o que o morph-spec guarda dentro do `.git` (backups do finish, registros de porta e de claim), que não aparece em `git status`: `✓` normalmente, `⚠` quando há backup legado esperando `--prune`, backup fora da retenção ou `.env*` além do prazo curto (com o comando de poda) — nunca `✗`, e nenhuma linha quando não há nada guardado.
|
|
@@ -64,7 +63,8 @@
|
|
|
64
63
|
**Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
|
|
65
64
|
- **`morph-spec doctor --procs [--prune] [--json]`** — o que o morph-spec e as suas sessões deixam de pé na máquina, em superfície própria: **não entra no `--full` nem no default** (mesmo motivo do `--ai`), e combinar com `--full`/`--mcp`/`--worktrees`/`--reset`/`--ai` é **erro de uso**. Sem `--prune` é **somente leitura**. **Processos:** lista `node`, `dotnet`, `testhost`, `esbuild` e Chrome headless de automação (o seu Chrome comum não entra) **sem pai vivo**, com idade e RSS. Órfão = pai ausente da tabela **ou criado depois do filho** (PID reaproveitado). **Nunca é candidato à poda:** processo com menos de 15 min ou de idade desconhecida; linha de comando que casa com **qualquer MCP configurado** (`.mcp.json`, `~/.claude.json` do projeto e do usuário, `.mcp.json` dos plugins habilitados), mesmo órfão, porque no Windows a cadeia `claude→cmd→npx→node` perde o elo quando o `cmd` do meio morre; o worker de refresh do grafo; e os nós de build (MSBuild `/nodemode`, VBCSCompiler), compartilhados entre sessões — esses saem por `dotnet build-server shutdown` quando ociosos, nunca por kill. `--prune` encerra **por PID** só o que sobrou, relendo o início e a linha de comando daquele PID imediatamente antes (PID reaproveitado é pulado); **nunca por nome de imagem** (`taskkill /IM node.exe` derrubaria os MCPs de todas as sessões). Exit 1 só se algum encerramento falhar. **MCP duplicado:** o mesmo servidor no `.mcp.json`/`~/.claude.json` e entregue por plugin habilitado sobe dois processos (`mcp__X` e `mcp__plugin_X_X`); o relatório só sugere remover do `.mcp.json` ou desligar o plugin. O `init` e o `mcp setup --auto` deixaram de recriar a duplicata: um plugin habilitado que entrega o servidor conta como instalado. **WSL:** `%USERPROFILE%\.wslconfig` ausente, ou sem `memory=` (seção `[wsl2]`) ou sem `autoMemoryReclaim=` (seção `[experimental]`), vira sugestão com o trecho e o `wsl --shutdown`; o morph-spec **nunca escreve fora do repositório**. O `--json` traz `procs`, `mcp`, `wsl` e, com `--prune`, `prune`. Listagem indisponível (PowerShell falhou, plataforma sem listador) sai **não verificado** com razão, nunca lista vazia.
|
|
66
65
|
- **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`). Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível. **Sobras de sub-agent:** achado `agent-worktree-leftover` (`info`, nunca falha): `reapable` (fix: `morph-spec worktree reap`) ou preservada com a razão (`dirty`, `exclusive-commits`, `no-usage-event`, `directory-missing`, `unknown`); agente ativo e agente dentro da graça não geram achado, e a árvore não é reportada de novo como `unintegrated-work`. O `--prune` também roda o `worktree reap` (`prune.agentReap` no `--json`).
|
|
67
|
-
- **`morph-spec report-check <feature> [task...] [--before-eval] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache; relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns` — sai `report-incomplete` com `partial: true` e ação `resume`: retome o agente, NÃO carimbe DEGRADADO. O marcador é lido de propósito, nunca inferido da falta de
|
|
66
|
+
- **`morph-spec report-check <feature> [task...] [--before-eval] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache; relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns` — sai `report-incomplete` com `partial: true` e ação `resume`: retome o agente, NÃO carimbe DEGRADADO. O marcador é lido de propósito, nunca inferido da falta de um item). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Vários ids (`report-check {f} T1 T2 --json`) auditam um batch paralelo numa chamada: sem task, o modo feature só audita tasks `done`, e as de um batch recém-voltado ainda não estão. **Bloco `tree`**, em toda saída e sem mudar veredito nem 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), `markers` (linhas com `MUTACAO` em arquivo rastreado ou não, fora dos ignorados), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde (fora de repositório ou erro) — desconhecido, nunca árvore limpa. **`--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 do avaliador, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`: os arquivos cujo conteúdo não é o da foto, inclusive o que foi commitado desde então. É assim que uma sessão que retoma depois de o avaliador morrer no meio vê a mutação que ficou; sem foto, `sinceBeforeEval` é `null`. É o que o orquestrador lê depois de um dispatch, em vez de `git status`/`git diff`/`git log`/`grep`, e o que cola no recado de retomada. Relatório completo = prosa do que foi feito, uma lista de **Arquivos:** e os **Critérios:** (cada `doneCriteria` com a evidência). Com `--before-eval`, a saída traz também `evaluatorModel` (`sonnet` por padrão, `opus` quando o diff toca auth, segredo, pagamento, migração, prompt/modelo ou mídia gerada, com `reasons`). Protocolo completo em MORPH.md §6.
|
|
67
|
+
- **`morph-spec scout-check <mapa> [--root <dir>] [--json]`** — audita o mapa do `morph-scout` antes de o plano usá-lo. Para cada `caminho:linha` ou `caminho:l1-l2` (em crase ou solto; contrabarra Windows e absoluto aceitos): o arquivo existe sob a raiz (`--root`, senão o topo da worktree corrente — nunca a raiz primária, cujo conteúdo é outro), as linhas existem, e o trecho em crase que segue a citação na mesma linha aparece nelas com tolerância de ±2 linhas (`...` separa fragmentos que têm de aparecer em ordem). Só o nome do arquivo vale quando é único na árvore (`git ls-files`, rastreado ou não); ambíguo reprova listando os candidatos. Linha `caminho:N:conteúdo` num bloco cercado (saída de grep) é conferida contra a linha N. Veredito de ausência ou de vida (VIVO/MORTO, "sem consumidor", "nenhum teste", "ninguém escreve"…) só passa sob um heading Inferido/Hipótese (ou com o marcador na linha), com o comando de busca em crase e a contagem bruta; o checker NÃO reconta a busca — o orquestrador confere com grep próprio o que muda o plano. Mapa sem nenhuma citação reprova (`no-citations`). Falhas: `missing-file`, `ambiguous-path`, `line-out-of-range`, `missing-quote`, `quote-mismatch`, `verdict-outside-inference`, `verdict-without-command`, `verdict-without-count`, `no-citations`. Sai 0 só sem falha, 1 com falha, 2 com o mapa ausente. Caminho canônico do mapa: `.morph/features/{f}/2-plan/scout-map.md`. Protocolo em MORPH.md §6.
|
|
68
68
|
|
|
69
69
|
## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
|
|
70
70
|
|
|
@@ -140,7 +140,10 @@ mesmo código de saída. Por isso ele entra como um **sétimo valor de `runner`*
|
|
|
140
140
|
`MORPH_EVAL_PIN`. O caminho do sumário é escolhido **pelo CLI** (arquivo temporário) para que dois nós
|
|
141
141
|
de eval no mesmo `verify` não colidam, e `MORPH_EVAL_PIN` leva só os pacotes que decidem o **formato**
|
|
142
142
|
da cache — a família `Microsoft.Extensions.AI` — para que subir um pacote de provider não invalide uma
|
|
143
|
-
árvore por nada.
|
|
143
|
+
árvore por nada. O pin vem do `.morph/framework/ai-pin.json` do projeto e, sem ele, do pacote da CLI (o
|
|
144
|
+
`update` não o copia, então o normal é ser o do pacote — o mesmo que o `doctor --ai` usa). Subir o pin
|
|
145
|
+
do pacote faz toda cache carimbada no pin anterior cair em `eval-cache-stale-pin`: regrave com
|
|
146
|
+
`verify --refresh-cache`.
|
|
144
147
|
|
|
145
148
|
**Os estados da cache são julgados ANTES de o comando rodar**, e todos os quatro são `fail` nomeado,
|
|
146
149
|
**nunca** `skip`:
|
|
@@ -195,8 +198,7 @@ string.
|
|
|
195
198
|
eval verde carimbado no mesmo commit faz o gate de `review` pausar, com a razão nomeando o caminho
|
|
196
199
|
tocado. `evalsStatus` e `promptModel` são **fatos de disco** e **não** entram em
|
|
197
200
|
`OVERRIDABLE_SIGNAL_KEYS`: um `--signals` capaz de afirmar "o eval rodou" derrotaria a política que a
|
|
198
|
-
regra existe para tornar determinística.
|
|
199
|
-
continua sendo, exclusivamente, o token gravado pelo avaliador independente.
|
|
201
|
+
regra existe para tornar determinística.
|
|
200
202
|
|
|
201
203
|
Conteúdo (o que é um bom dataset, o que entra na chave de cache, TTL, o que se commita):
|
|
202
204
|
`ai-agents/evals-with-cache.md`.
|
|
@@ -258,8 +260,10 @@ fallback sintetiza exigiria inventar a declaração que o comando veio manter).
|
|
|
258
260
|
linha de comando é erro de uso (exit 2): baseline é da frota inteira, e o escopo de task recorta nós e testes. Verde com gravação
|
|
259
261
|
recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
|
|
260
262
|
pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
|
|
261
|
-
testes rodaram.
|
|
262
|
-
|
|
263
|
+
testes rodaram. A execução que **gravou** a baseline (verde e inteira) persiste o stamp `verification` como qualquer verify de
|
|
264
|
+
feature: ela mediu a suíte toda, e rodá-la de novo só para ter prova seria pagar duas vezes pelo mesmo
|
|
265
|
+
verde. Sem gravação (vermelha, parcial, contagem desconhecida) não há stamp, e o `reason` do `--json`
|
|
266
|
+
nomeia o nó que impediu. **Consequência a conhecer:** um `expectedCount` digitado à
|
|
263
267
|
mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
|
|
264
268
|
`--record-baseline` recusa — preservando o 9. **Não há flag de regravação forçada:** rebaixe ou apague
|
|
265
269
|
o número à mão e então re-meça pelo comando. O caso oposto se conserta sozinho — número baixo demais
|
|
@@ -280,8 +284,9 @@ passa com aviso de alta, e o comando grava.
|
|
|
280
284
|
> relógios que o CLI de fato tem: build **120 000 ms**, tests **120 000 ms por projeto de teste
|
|
281
285
|
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
|
|
282
286
|
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
283
|
-
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:**
|
|
284
|
-
>
|
|
287
|
+
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** no loop, o
|
|
288
|
+
> recorte por task (ou `--filter`) a evita; no Gate 3, rode `verify {feature}` com `run_in_background` e
|
|
289
|
+
> espere a notificação de término — nunca `sleep`/`until` consultando o processo.
|
|
285
290
|
> Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum
|
|
286
291
|
> teste falhou`) em vez de devolver log truncado.
|
|
287
292
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Checklist do avaliador — Gate 3
|
|
2
|
+
|
|
3
|
+
Você não dá nota. Você responde: **esta entrega faz o que os use cases dizem, sem quebrar o que já
|
|
4
|
+
funcionava?** E prova cada "não" com evidência.
|
|
5
|
+
|
|
6
|
+
## Saída (`4-review/evaluator-report.md`)
|
|
7
|
+
|
|
8
|
+
```markdown
|
|
9
|
+
**Verdict:** SHIP ← ou FIX
|
|
10
|
+
|
|
11
|
+
## Bloqueadores
|
|
12
|
+
|
|
13
|
+
| # | Tipo | Onde | Evidência |
|
|
14
|
+
|---|------|------|-----------|
|
|
15
|
+
| B1 | uc | src/Orders/Create.cs:42 | UC-03 P2: pedido sem itens é aceito — `POST /orders {items:[]}` → 201 |
|
|
16
|
+
|
|
17
|
+
## Notas
|
|
18
|
+
|
|
19
|
+
- (o que melhoraria, sem bloquear)
|
|
20
|
+
|
|
21
|
+
## Navegado
|
|
22
|
+
|
|
23
|
+
- (fluxos exercitados na app, quando o stack estava no ar; o que viu no console/network)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sem bloqueador, escreva `Nenhum.` na seção. `SHIP` com bloqueador aberto é contradição e pausa o gate.
|
|
27
|
+
|
|
28
|
+
## O que bloqueia — só com evidência de um destes tipos
|
|
29
|
+
|
|
30
|
+
| Tipo | Quando | Evidência mínima |
|
|
31
|
+
|------|--------|------------------|
|
|
32
|
+
| `uc` | pós-condição de UC ou `doneCriteria` não atendida | repro (comando, request, teste) ou o trecho que prova |
|
|
33
|
+
| `teste` | teste vermelho; teste novo que não morde a regra que diz proteger | saída do verify, mutante sobrevivente, ou o assert que passa pelo motivo errado |
|
|
34
|
+
| `regressao` | comportamento que funcionava e quebrou | teste ou repro contra o comportamento anterior |
|
|
35
|
+
| `seguranca` | segredo exposto, authz faltando, injeção, dado pessoal vazando, custo sem teto | o caminho de exploração |
|
|
36
|
+
|
|
37
|
+
Todo o resto — estilo, nome, estrutura, "eu faria diferente" — é **nota**. Nota não bloqueia e não
|
|
38
|
+
pede rodada nova. Na dúvida entre bloqueador e nota, sem evidência tipada, é nota.
|
|
39
|
+
|
|
40
|
+
## O que procurar, em ordem de dano
|
|
41
|
+
|
|
42
|
+
1. **Cada pós-condição de UC / `doneCriteria`**: onde no código ela vale, e qual teste a prova.
|
|
43
|
+
2. **Fiação**: existe caminho da entrada real ao efeito observável? A camada pura testada não prova
|
|
44
|
+
que o handler a chama. Inventarie quem consome o eixo novo.
|
|
45
|
+
3. **Teste que não morde**: nasceu verde e afirma o que já era verdade antes da mudança; só testa
|
|
46
|
+
recusa e nunca o caminho de escrita; guard cujos dois lados passam pelo mesmo serializador;
|
|
47
|
+
asserção sobre texto que mede vocabulário, não sentido.
|
|
48
|
+
4. **Verificações mecânicas** (.NET minimal API / Next): todo serviço injetado numa lambda de endpoint
|
|
49
|
+
tem registro em `Program.cs` (senão a app não sobe); prefixo de rota server-direct e de proxy são
|
|
50
|
+
constantes distintas, cada uma usada só do seu lado.
|
|
51
|
+
5. **Correção de rodada anterior**: o que ela abriu? Releia o comportamento vizinho.
|
|
52
|
+
6. **Fora do diff**: o blast radius que o orquestrador anexou (`graph affected`).
|
|
53
|
+
|
|
54
|
+
## Prova por mutação — opcional, no máximo 3
|
|
55
|
+
|
|
56
|
+
Só quando o diff tem uma linha de guarda ou regra que um UC exige e você não achou o teste que a
|
|
57
|
+
morde. Escreva `4-review/mutants.json` e rode `morph-spec mutate {feature}` **uma vez**:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a",
|
|
61
|
+
"expect":"OrdersTests.RecusaSemItens deve falhar: a regra de itens some"}]}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`expect` é obrigatório: diga qual teste deve falhar e por quê **antes** de rodar — mutante cujo
|
|
65
|
+
efeito você não sabe prever tende a ser equivalente. Barra invertida do código vira `{{BS}}`.
|
|
66
|
+
Sobreviveu numa linha que um UC exige → bloqueador `teste`. Sobreviveu em outra → nota. Cole a seção
|
|
67
|
+
`## Mutações` que a ferramenta imprime. A reversão é da ferramenta; rodada morta no meio:
|
|
68
|
+
`morph-spec mutate --restore`.
|
|
69
|
+
|
|
70
|
+
## Condicionais
|
|
71
|
+
|
|
72
|
+
**Feature que gera mídia** (alguma task cita em `standards[]` `standards/ai-agents/modalities-image-gen.md`,
|
|
73
|
+
`media-video.md` ou `media-pipeline.md`): grave `## Mídia gerada`, uma linha por afirmação — custo
|
|
74
|
+
controlado, revisão humana, mídia marcada, hot path não bloqueado —, cada uma com o código que a prende
|
|
75
|
+
e o teste que a prova. Afirmação falsa é bloqueador. Sem a seção, o gate pausa.
|
|
76
|
+
|
|
77
|
+
**Feature que chama LLM**: dá para reconstruir o que foi ao provedor e o que ele respondeu neste turno?
|
|
78
|
+
Não → nota, ou bloqueador quando um UC exige rastreabilidade.
|
|
79
|
+
|
|
80
|
+
**Prompt ou modelo alterado**: o nó `evals` do verify é a prova; sem ele, o gate pausa sozinho.
|
|
81
|
+
|
|
82
|
+
**Contrato de tela** (`1-design/mockups.md` com `## Tela:`): grave `## Checklist de tela`, uma linha por
|
|
83
|
+
item decidido de cada tela:
|
|
84
|
+
|
|
85
|
+
| Tela | Item | Mockup (planejado) | Entregue (observado) | Veredito |
|
|
86
|
+
|
|
87
|
+
Veredito `cumpre` / `diverge` / `não verificável` (com o motivo). Paginação, ordenação ou filtro que o
|
|
88
|
+
checklist põe no servidor e a tela faz em memória, ou estado vazio/carregando/erro decidido e
|
|
89
|
+
ausente, é bloqueador `uc`. Divergência registrada em `decisions.md` é nota. Sem a seção, havendo
|
|
90
|
+
item decidido, o gate pausa.
|
|
@@ -24,13 +24,6 @@ Um arquivo por fase. Se a fase for executada em múltiplas sessões, o mesmo arq
|
|
|
24
24
|
"standards_consulted": ["id-do-standard-1"],
|
|
25
25
|
"agent_persona": "dotnet-senior | nextjs-expert | etc",
|
|
26
26
|
"sub_agent_dispatched": false,
|
|
27
|
-
"eval_score": 8.5,
|
|
28
|
-
"eval_dimensions": {
|
|
29
|
-
"architecture": 9.0,
|
|
30
|
-
"contracts": 8.0,
|
|
31
|
-
"code_quality": 8.5,
|
|
32
|
-
"test_coverage": 8.5
|
|
33
|
-
},
|
|
34
27
|
"files_modified": ["path/relativo/arquivo.cs"],
|
|
35
28
|
"duration_seconds": null
|
|
36
29
|
}
|
|
@@ -38,7 +31,6 @@ Um arquivo por fase. Se a fase for executada em múltiplas sessões, o mesmo arq
|
|
|
38
31
|
"phase_summary": {
|
|
39
32
|
"tasks_completed": 3,
|
|
40
33
|
"tasks_failed": 0,
|
|
41
|
-
"avg_eval_score": 8.3,
|
|
42
34
|
"standards_most_used": ["architecture-vertical-slice"],
|
|
43
35
|
"sub_agents_dispatched": 0
|
|
44
36
|
}
|
|
@@ -49,20 +41,18 @@ Um arquivo por fase. Se a fase for executada em múltiplas sessões, o mesmo arq
|
|
|
49
41
|
|
|
50
42
|
O trace é **gerado pelo hook `trace-autogen`** (PostToolUse em Write|Edit) a cada
|
|
51
43
|
write de `2-plan/tasks.json` — o último write da cerimônia de cada task. As skills
|
|
52
|
-
(`morph-implement`, `morph-
|
|
44
|
+
(`morph-implement`, `morph-review`) **não escrevem o trace**.
|
|
53
45
|
|
|
54
46
|
| Fonte | O que alimenta |
|
|
55
47
|
|-------|----------------|
|
|
56
48
|
| `tasks.json` | conjunto de tasks, `status` (done→completed, skipped→skipped, blocked→failed; pending/in_progress ficam fora), `outputs` → `files_modified` |
|
|
57
49
|
| `recap.md` | `**Standard:**` → `standards_consulted`; `**Persona:**` → `agent_persona` + `sub_agent_dispatched` (`morph-{id}`/`general-purpose` = true) |
|
|
58
|
-
| `feature.json → taskScores` (gravado por `morph-spec score`) | `eval_score` (lastScore) e `eval_dimensions` (camelCase → snake_case) |
|
|
59
50
|
|
|
60
51
|
`completed_at` e `phase_summary` são preenchidos quando nenhuma task resta
|
|
61
52
|
`pending`/`in_progress`. O arquivo existente da mesma feature+fase é atualizado
|
|
62
53
|
in place (`session_id`/`started_at` preservados entre sessões).
|
|
63
54
|
|
|
64
|
-
**Se o trace parecer incompleto, corrija a FONTE** (um `
|
|
65
|
-
faltante, um recap sem o bloco `## T{N}`), nunca o trace.
|
|
55
|
+
**Se o trace parecer incompleto, corrija a FONTE** (um recap sem o bloco `## T{N}`), nunca o trace.
|
|
66
56
|
|
|
67
57
|
## Regras de geração
|
|
68
58
|
|
|
@@ -54,9 +54,8 @@ framework/hooks/
|
|
|
54
54
|
| **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
|
|
55
55
|
| **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), bases a task tree on the dispatcher's `morph/<f>` branch when `worktree.baseRef` is unset (`"head"` = current HEAD, `"fresh"` = default branch), prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
|
|
56
56
|
| **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
|
|
57
|
-
| **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success. Also touches `.morph/memory/build-dirty.json` on every source-file edit — the write side of validate-completion.js's Stop-hook build-dirty gate |
|
|
58
57
|
| **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
|
|
59
|
-
| **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md
|
|
58
|
+
| **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md on every tasks.json write — the skills never write traces by hand |
|
|
60
59
|
| **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (UC/ADR/task/gate), 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 |
|
|
61
60
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
62
61
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions; logs one `coherent`/`divergent` line per check in `.morph/logs/activity.jsonl` (issue #63) |
|
|
@@ -67,7 +66,7 @@ framework/hooks/
|
|
|
67
66
|
| **PostToolBatch** | telemetry-log.js | Telemetry | One `ToolBatch` event per model turn that used tools (the turn counter of `cost --metrics`) |
|
|
68
67
|
| **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 |
|
|
69
68
|
| **UserPromptSubmit** | telemetry-log.js | Telemetry | Appends the user's prompt to the same `events.jsonl` stream; silent, fail-open |
|
|
70
|
-
| **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift (hash-deduped, only re-injects when the warning set changes)
|
|
69
|
+
| **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift (hash-deduped, only re-injects when the warning set changes) |
|
|
71
70
|
|
|
72
71
|
## Design Principles
|
|
73
72
|
|
|
@@ -61,7 +61,7 @@ export async function run(payload, deps = {}) {
|
|
|
61
61
|
if (result.status !== 'busy') return null;
|
|
62
62
|
|
|
63
63
|
return blockResult(
|
|
64
|
-
`⏳ \`${kind}\` não rodou: ${describeBusy(result, config)}. Não é falha do código —
|
|
64
|
+
`⏳ \`${kind}\` não rodou: ${describeBusy(result, config)}. Não é falha do código — a fila já esperou o prazo dela: não repita em laço; siga sem esta execução e avise o usuário. `
|
|
65
65
|
+ '`morph-spec heavy status` mostra quem segura os slots; `heavy.gateBash: false` desliga esta espera.',
|
|
66
66
|
);
|
|
67
67
|
} catch {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
7
|
*
|
|
8
8
|
* Blocks Write/Edit to spec artifacts after their approval gate has passed:
|
|
9
|
-
* - 2-plan/spec.md,
|
|
9
|
+
* - 2-plan/spec.md, etc. → blocked if 'plan' gate approved
|
|
10
10
|
* - 1-design/design-system.md, etc. → blocked if 'uiux' gate approved
|
|
11
11
|
*
|
|
12
12
|
* Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
|
|
@@ -340,7 +340,7 @@ export async function run(payload) {
|
|
|
340
340
|
);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
// ── Arquivo totalmente travado (spec.md,
|
|
343
|
+
// ── Arquivo totalmente travado (spec.md, design) ────────────
|
|
344
344
|
// O corpo continua congelado. A ÚNICA passagem é a errata append-only:
|
|
345
345
|
// corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
|
|
346
346
|
// de leitura/reconstrução cai no bloqueio (o conservador aqui é BLOQUEAR —
|
|
@@ -86,7 +86,7 @@ function capField(value) {
|
|
|
86
86
|
function classifyReadKind(target) {
|
|
87
87
|
const t = (target || '').toLowerCase();
|
|
88
88
|
|
|
89
|
-
if (t.includes('evals/
|
|
89
|
+
if (t.includes('evals/review-checklist')) return 'rubric';
|
|
90
90
|
if (t.includes('standards/')) return 'standard';
|
|
91
91
|
if (t.endsWith('skill.md') || t.includes('/skills/')) return 'skill';
|
|
92
92
|
if (t.split('/').pop() === 'agents.json') return 'agents';
|
|
@@ -95,7 +95,7 @@ function classifyReadKind(target) {
|
|
|
95
95
|
if (inFeature) {
|
|
96
96
|
const rel = inFeature[1];
|
|
97
97
|
const PLAN_ARTIFACT_BASENAMES = new Set([
|
|
98
|
-
'proposal.md', 'spec.md', '
|
|
98
|
+
'proposal.md', 'spec.md', 'tasks.json',
|
|
99
99
|
'decisions.md', 'recap.md', 'handoff.md',
|
|
100
100
|
]);
|
|
101
101
|
const base = rel.split('/').pop();
|